feat(rotator): add Hy-Gain DCU-1 backend (RotorCard DXA, Rotor-EZ, Green Heron)

New internal/rotator/dcu1 client speaking the DCU-1 command set (AP1nnn / AM1
to go-to, AI1 to read bearing), over a serial COM port (4800 baud default) or a
raw TCP serial-over-IP bridge. Azimuth-only; Stop re-commands the current
bearing since the base set has no stop opcode.

Wired through app.go (normRotorType, default port 4001, GoTo/Heading/Stop/test
switches, park returns the same "PstRotator only" note as ARCO) and exposed in
the rotor settings as "Hy-Gain DCU-1 (RotorCard DXA, Rotor-EZ, Green Heron)"
with the ARCO's serial/TCP transport chooser and a bilingual hint. Protocol is
implemented from the standard DCU-1 spec and still needs field confirmation
against a real RotorCard DXA.
This commit is contained in:
2026-08-06 10:40:36 +02:00
parent 20dc3e83a6
commit 82ce3353c4
5 changed files with 215 additions and 14 deletions
+38 -1
View File
@@ -52,6 +52,7 @@ import (
"hamlog/internal/qso"
"hamlog/internal/relaydev"
"hamlog/internal/rigctld"
"hamlog/internal/rotator/dcu1"
"hamlog/internal/rotator/gs232"
"hamlog/internal/rotator/pst"
"hamlog/internal/rotgenius"
@@ -13477,7 +13478,7 @@ type logicalRotor struct {
// normRotorType clamps a rotor type to a known backend.
func normRotorType(t string) string {
if t == "rotgenius" || t == "arco" {
if t == "rotgenius" || t == "arco" || t == "dcu1" {
return t
}
return "pst"
@@ -13490,6 +13491,8 @@ func rotatorDefaultPort(typ string) int {
return 9006 // 4O3A native default
case "arco":
return 4001 // placeholder — the real number is set in ARCO's LAN menu
case "dcu1":
return 4001 // only used with a serial-over-IP bridge; DCU-1 has no standard
default:
return 12000 // PstRotator UDP
}
@@ -13682,6 +13685,16 @@ func arcoClient(l rotorLink) *gs232.Client {
return gs232.New(l.Host, l.Port)
}
// dcu1Client builds the Hy-Gain DCU-1 client for a rotor's transport: the
// controller's COM port (the usual case — RotorCard DXA, Green Heron, Rotor-EZ)
// or a serial-over-IP bridge on TCP.
func dcu1Client(l rotorLink) *dcu1.Client {
if l.Transport == "serial" {
return dcu1.NewSerial(l.ComPort, l.Baud)
}
return dcu1.New(l.Host, l.Port)
}
// RotatorHeading is the live antenna heading for the status bar and compass.
type RotatorHeading struct {
Enabled bool `json:"enabled"`
@@ -13748,6 +13761,16 @@ func (a *App) GetRotatorHeading() RotatorHeading {
base.Azimuth = az
base.Raw = raw
return base
case "dcu1":
az, raw, herr := dcu1Client(link).Heading()
if herr != nil {
base.Raw = herr.Error()
return base
}
base.OK = true
base.Azimuth = az
base.Raw = raw
return base
default:
az, raw, herr := pst.New(link.Host, link.Port).Heading()
if herr != nil {
@@ -13774,6 +13797,8 @@ func (a *App) RotatorGoTo(az int, el int) error {
return rotgenius.New(link.Host, link.Port).GoTo(link.Num, az)
case "arco":
return arcoClient(link).GoTo(az)
case "dcu1":
return dcu1Client(link).GoTo(az)
default:
return pst.New(link.Host, link.Port).GoTo(az, link.HasElevation, el)
}
@@ -13791,6 +13816,8 @@ func (a *App) RotatorStop() error {
return rotgenius.New(link.Host, link.Port).Stop()
case "arco":
return arcoClient(link).Stop()
case "dcu1":
return dcu1Client(link).Stop()
default:
return pst.New(link.Host, link.Port).Stop()
}
@@ -13809,6 +13836,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 "dcu1":
return fmt.Errorf("park is a PstRotator feature; not available over the DCU-1 link")
default:
return pst.New(link.Host, link.Port).Park()
}
@@ -13847,6 +13876,14 @@ func testRotorLink(l rotorLink) error {
// GS-232 — without moving the antenna.
_, _, err := arcoClient(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")
}
// A bearing query (AI1) proves the link and the DCU-1 command set without
// moving the antenna.
_, _, err := dcu1Client(l).Heading()
return err
default:
return pst.New(l.Host, l.Port).GoTo(0, false, -1)
}