feat(sat): follow the azimuth only
A satellite tracker that insists on an elevation motor is a tracker switched off for nearly everybody. A pass at the edge of the footprint — which is most of them — never climbs above ten or fifteen degrees for its whole length, and a yagi's beamwidth swallows that: the bearing alone is enough, and it is how most stations that work satellites are actually built. The same switch rescues an az/el station whose elevation motor has failed. So it is an option, not a silent fallback, because it does cost something: a bird straight overhead is a moving azimuth and a bearing that means nothing, and whether to accept that is the operator's call. With it on, any rotor in the list can be chosen — the PstRotator, the Rotator Genius, the ARCO, the tower already turned for HF. That works because the per-backend command dispatch moved out of the three RotatorGoTo/Stop/Heading methods into linkGoTo/linkStop/linkHeading, so the satellite tracker drives any of the seven backends through the same code the compass uses instead of a second implementation of each. GetRotatorHeading loses sixty lines of near-duplicate switch in the process, and a rotor with no elevation axis now says so (HasElevation) rather than reporting a zero that looks like a real bearing. One trap, with a test on it: the step check compared both axes, so with the elevation never commanded its difference stayed above the step for the whole pass and every tick ordered the antenna to the bearing it was already on. A mast has a finite number of turns in it. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
@@ -17412,6 +17412,126 @@ func (a *App) activeRotor() (lr logicalRotor, rotors []logicalRotor, idx int, ok
|
||||
return rotors[idx], rotors, idx, true
|
||||
}
|
||||
|
||||
// linkHeading asks one rotor where it is.
|
||||
//
|
||||
// The per-backend switch lives here, once, so the compass and the satellite
|
||||
// tracker read a rotor the same way. hasEl says whether the elevation returned
|
||||
// means anything: an azimuth controller answers the azimuth question perfectly
|
||||
// well and has nothing to say about the other axis, and reporting a zero there
|
||||
// would draw an antenna lying on the horizon.
|
||||
//
|
||||
// raw is the controller's own reply, kept for the log — it is what tells a
|
||||
// baffled operator whether the port is silent or answering something we did not
|
||||
// expect.
|
||||
func linkHeading(l rotorLink) (az, el float64, hasEl bool, raw string, err error) {
|
||||
switch l.Type {
|
||||
case "rotgenius":
|
||||
st, r, herr := rotgenius.New(l.Host, l.Port).Heading(l.Num)
|
||||
if herr != nil {
|
||||
return 0, 0, false, "", herr
|
||||
}
|
||||
if !st.Connected {
|
||||
return 0, 0, false, "sensor not connected (999)", fmt.Errorf("sensor not connected")
|
||||
}
|
||||
return float64(st.Azimuth), 0, false, r, nil
|
||||
case "arco":
|
||||
v, r, herr := arcoClient(l).Heading()
|
||||
return float64(v), 0, false, r, herr
|
||||
case "erc":
|
||||
aa, ee, r, herr := ercClient(l).Position()
|
||||
return float64(aa), float64(ee), herr == nil, r, herr
|
||||
case "easycomm":
|
||||
aa, ee, live, herr := easycommClient(l).Heading()
|
||||
if herr != nil {
|
||||
return 0, 0, false, "", herr
|
||||
}
|
||||
r := fmt.Sprintf("AZ %.0f° EL %.0f°", aa, ee)
|
||||
if !live {
|
||||
// The controller answered nothing and this is the last COMMANDED
|
||||
// position. Say so: a stuck rotator must not be able to hide behind
|
||||
// an order it never carried out.
|
||||
r += " (commanded)"
|
||||
}
|
||||
return aa, ee, true, r, nil
|
||||
case "spid":
|
||||
aa, ee, herr := spidClient(l).Heading()
|
||||
if herr != nil {
|
||||
return 0, 0, false, "", herr
|
||||
}
|
||||
if l.HasElevation {
|
||||
return float64(aa), float64(ee), true, fmt.Sprintf("AZ %d° EL %d°", aa, ee), nil
|
||||
}
|
||||
return float64(aa), 0, false, fmt.Sprintf("%d°", aa), nil
|
||||
case "dcu1":
|
||||
v, r, herr := dcu1Client(l).Heading()
|
||||
return float64(v), 0, false, r, herr
|
||||
default:
|
||||
v, r, herr := pst.New(l.Host, l.Port).Heading()
|
||||
if herr != nil {
|
||||
// PstRotator's own text is more useful than the transport error.
|
||||
return 0, 0, false, r, herr
|
||||
}
|
||||
if l.HasElevation {
|
||||
if e, _, eerr := pst.New(l.Host, l.Port).Elevation(); eerr == nil {
|
||||
return float64(v), float64(e), true, r, nil
|
||||
}
|
||||
}
|
||||
return float64(v), 0, false, r, nil
|
||||
}
|
||||
}
|
||||
|
||||
// linkGoTo points one rotor. An elevation below zero is the callers' "no
|
||||
// opinion" — a spot click, a compass drag — and leaves the elevation axis where
|
||||
// it is rather than swinging a dish to the horizon.
|
||||
func linkGoTo(l rotorLink, az, el int) error {
|
||||
switch l.Type {
|
||||
case "rotgenius":
|
||||
return rotgenius.New(l.Host, l.Port).GoTo(l.Num, az)
|
||||
case "arco":
|
||||
return arcoClient(l).GoTo(az)
|
||||
case "erc":
|
||||
if el < 0 {
|
||||
return ercClient(l).GoTo(az)
|
||||
}
|
||||
return ercClient(l).GoToAzEl(az, el)
|
||||
case "easycomm":
|
||||
if el < 0 {
|
||||
if _, cur, _, err := easycommClient(l).Heading(); err == nil {
|
||||
el = int(math.Round(cur))
|
||||
} else {
|
||||
el = 0
|
||||
}
|
||||
}
|
||||
return easycommClient(l).Point(float64(az), float64(el))
|
||||
case "spid":
|
||||
return spidClient(l).GoTo(az, el)
|
||||
case "dcu1":
|
||||
return dcu1Client(l).GoTo(az)
|
||||
default:
|
||||
return pst.New(l.Host, l.Port).GoTo(az, l.HasElevation, el)
|
||||
}
|
||||
}
|
||||
|
||||
// linkStop interrupts one rotor.
|
||||
func linkStop(l rotorLink) error {
|
||||
switch l.Type {
|
||||
case "rotgenius":
|
||||
return rotgenius.New(l.Host, l.Port).Stop()
|
||||
case "arco":
|
||||
return arcoClient(l).Stop()
|
||||
case "erc":
|
||||
return ercClient(l).Stop()
|
||||
case "easycomm":
|
||||
return easycommClient(l).Stop()
|
||||
case "spid":
|
||||
return spidClient(l).Stop()
|
||||
case "dcu1":
|
||||
return dcu1Client(l).Stop()
|
||||
default:
|
||||
return pst.New(l.Host, l.Port).Stop()
|
||||
}
|
||||
}
|
||||
|
||||
// GetRotatorHeading queries the active rotor for its azimuth. Returns
|
||||
// Enabled=false when no rotator is configured. Polled by the status bar.
|
||||
func (a *App) GetRotatorHeading() RotatorHeading {
|
||||
@@ -17424,94 +17544,19 @@ func (a *App) GetRotatorHeading() RotatorHeading {
|
||||
names[i] = r.Name
|
||||
}
|
||||
base := RotatorHeading{Enabled: true, Rotors: names, Active: idx, Motorized: lr.Motorized}
|
||||
link := lr.Link
|
||||
switch link.Type {
|
||||
case "rotgenius":
|
||||
st, raw, herr := rotgenius.New(link.Host, link.Port).Heading(link.Num)
|
||||
if herr != nil {
|
||||
base.Raw = herr.Error()
|
||||
return base
|
||||
}
|
||||
if !st.Connected {
|
||||
base.Raw = "sensor not connected (999)"
|
||||
return base
|
||||
}
|
||||
base.OK = true
|
||||
base.Azimuth = st.Azimuth
|
||||
az, el, hasEl, raw, err := linkHeading(lr.Link)
|
||||
if err != nil {
|
||||
base.Raw = raw
|
||||
return base
|
||||
case "arco":
|
||||
az, raw, herr := arcoClient(link).Heading()
|
||||
if herr != nil {
|
||||
base.Raw = herr.Error()
|
||||
return base
|
||||
if base.Raw == "" {
|
||||
base.Raw = err.Error()
|
||||
}
|
||||
base.OK = true
|
||||
base.Azimuth = az
|
||||
base.Raw = raw
|
||||
return base
|
||||
case "erc":
|
||||
az, el, raw, herr := ercClient(link).Position()
|
||||
if herr != nil {
|
||||
base.Raw = herr.Error()
|
||||
return base
|
||||
}
|
||||
base.OK = true
|
||||
base.Azimuth, base.Elevation, base.HasElevation = az, el, true
|
||||
base.Raw = raw
|
||||
return base
|
||||
case "easycomm":
|
||||
az, el, live, herr := easycommClient(link).Heading()
|
||||
if herr != nil {
|
||||
base.Raw = herr.Error()
|
||||
return base
|
||||
}
|
||||
base.OK = true
|
||||
base.Azimuth, base.Elevation, base.HasElevation = int(math.Round(az)), int(math.Round(el)), true
|
||||
if live {
|
||||
base.Raw = fmt.Sprintf("AZ %.0f° EL %.0f°", az, el)
|
||||
} else {
|
||||
// The controller answered nothing and this is the last COMMANDED
|
||||
// position. Say so: a stuck rotator must not be able to hide behind
|
||||
// an order it never carried out.
|
||||
base.Raw = fmt.Sprintf("AZ %.0f° EL %.0f° (commanded)", az, el)
|
||||
}
|
||||
return base
|
||||
case "spid":
|
||||
az, el, 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)
|
||||
if link.HasElevation {
|
||||
base.Elevation, base.HasElevation = el, true
|
||||
base.Raw = fmt.Sprintf("AZ %d° EL %d°", az, el)
|
||||
}
|
||||
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 {
|
||||
base.Raw = raw
|
||||
return base
|
||||
}
|
||||
base.OK = true
|
||||
base.Azimuth = az
|
||||
base.Raw = raw
|
||||
return base
|
||||
}
|
||||
base.OK = true
|
||||
base.Azimuth = int(math.Round(az))
|
||||
base.Elevation, base.HasElevation = int(math.Round(el)), hasEl
|
||||
base.Raw = raw
|
||||
return base
|
||||
}
|
||||
|
||||
// RotatorGoTo points the active rotor at the given azimuth (and optional
|
||||
@@ -17535,36 +17580,7 @@ func (a *App) RotatorGoToPath(az int, el int, path string) error {
|
||||
if !ok {
|
||||
return fmt.Errorf("no rotator configured")
|
||||
}
|
||||
link := lr.Link
|
||||
switch link.Type {
|
||||
case "rotgenius":
|
||||
return rotgenius.New(link.Host, link.Port).GoTo(link.Num, az)
|
||||
case "arco":
|
||||
return arcoClient(link).GoTo(az)
|
||||
case "erc":
|
||||
// An elevation of -1 is the callers' "no opinion" (a spot click, a
|
||||
// compass drag). Leaving the elevation where it is beats swinging the
|
||||
// dish to the horizon because somebody clicked a DX spot.
|
||||
if el < 0 {
|
||||
return ercClient(link).GoTo(az)
|
||||
}
|
||||
return ercClient(link).GoToAzEl(az, el)
|
||||
case "easycomm":
|
||||
if el < 0 {
|
||||
if _, cur, _, err := easycommClient(link).Heading(); err == nil {
|
||||
el = int(math.Round(cur))
|
||||
} else {
|
||||
el = 0
|
||||
}
|
||||
}
|
||||
return easycommClient(link).Point(float64(az), float64(el))
|
||||
case "spid":
|
||||
return spidClient(link).GoTo(az, el)
|
||||
case "dcu1":
|
||||
return dcu1Client(link).GoTo(az)
|
||||
default:
|
||||
return pst.New(link.Host, link.Port).GoTo(az, link.HasElevation, el)
|
||||
}
|
||||
return linkGoTo(lr.Link, az, el)
|
||||
}
|
||||
|
||||
// RotatorStop interrupts any in-progress rotation of the active rotor.
|
||||
@@ -17573,23 +17589,7 @@ func (a *App) RotatorStop() error {
|
||||
if !ok {
|
||||
return fmt.Errorf("no rotator configured")
|
||||
}
|
||||
link := lr.Link
|
||||
switch link.Type {
|
||||
case "rotgenius":
|
||||
return rotgenius.New(link.Host, link.Port).Stop()
|
||||
case "arco":
|
||||
return arcoClient(link).Stop()
|
||||
case "erc":
|
||||
return ercClient(link).Stop()
|
||||
case "easycomm":
|
||||
return easycommClient(link).Stop()
|
||||
case "spid":
|
||||
return spidClient(link).Stop()
|
||||
case "dcu1":
|
||||
return dcu1Client(link).Stop()
|
||||
default:
|
||||
return pst.New(link.Host, link.Port).Stop()
|
||||
}
|
||||
return linkStop(lr.Link)
|
||||
}
|
||||
|
||||
// RotorPreset is one quick-turn button on the rotor widget: a short label and
|
||||
|
||||
Reference in New Issue
Block a user