feat(rotator): drive a Rotator Genius through the overlap
An operator with a 450° mast watched the Genius stop at 359 and had to press "clockwise" by hand to get through north. Half of that was ours: GoTo clamped every target to 360 before sending it, although the wire command carries three digits and always could have said 370. So the clamp goes to 450, the rotator-range setting is offered for the Rotator Genius like the other backends it applies to, and when a bearing can be reached two ways the nearer one is taken — 010° sent as 370° when the antenna is already at 350°, which is the entire point of having an overlap. THE GENIUS DECIDES WHAT IS REACHABLE. It reports the limits it is configured with, and they are the truth about what is bolted to the tower. This particular station's box says "5 to 4" — the factory 360° range — and would refuse 370, turning a working command into a rejected one. So the overlap is used only when the Genius itself says it has one, and when OpsLog is set to 450 while the Genius is not, the log says so once a minute and names the dialog to change: the setting lives in the Genius's own Rotator Configuration, and nothing here can reach past its limits. The |h reply always carried those limits and they were being skipped over. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
@@ -17486,7 +17486,7 @@ func linkHeading(l rotorLink) (az, el float64, hasEl bool, raw string, err error
|
||||
func linkGoTo(l rotorLink, az, el int) error {
|
||||
switch l.Type {
|
||||
case "rotgenius":
|
||||
return rotgenius.New(l.Host, l.Port).GoTo(l.Num, az)
|
||||
return rotgeniusGoTo(l, az)
|
||||
case "arco":
|
||||
return arcoClient(l).GoTo(az)
|
||||
case "erc":
|
||||
@@ -22517,3 +22517,77 @@ func (a *App) IcomConsolePTT(on bool) error {
|
||||
}
|
||||
return a.cat.SetPTT(on)
|
||||
}
|
||||
|
||||
// rotgeniusGoTo picks which way round to reach a bearing on a mast with an
|
||||
// overlap.
|
||||
//
|
||||
// A 450° rotator can be at 010° twice over: once at 10 and once at 370, and
|
||||
// only the second reaches it without unwinding the cable back through north.
|
||||
// OpsLog used to clamp every target to 360, so an operator with such a mast
|
||||
// watched the Rotator Genius stop dead at 359 and had to press "clockwise" by
|
||||
// hand to get through north.
|
||||
//
|
||||
// THE GENIUS DECIDES WHAT IS REACHABLE, not us and not a setting. It reports the
|
||||
// limits it is configured with, and they are the truth about what is bolted to
|
||||
// the tower: a box configured "5 to 4" — the factory 360° range — refuses a
|
||||
// target of 370, and asking anyway turns a working command into a rejected one.
|
||||
// So the overlap is used only when the Genius itself says it has one, and when
|
||||
// it does not, the operator is told, because the setting to change is in the
|
||||
// Genius and not here.
|
||||
//
|
||||
// Which of the two forms is right then depends on where the antenna IS, so the
|
||||
// heading is read first and the nearer one wins. That is the point of an
|
||||
// overlap: a beam at 350° heading for 010° should cross north, not travel the
|
||||
// other 340 degrees.
|
||||
func rotgeniusGoTo(l rotorLink, az int) error {
|
||||
c := rotgenius.New(l.Host, l.Port)
|
||||
a := ((az % 360) + 360) % 360
|
||||
if l.MaxAz <= 360 {
|
||||
return c.GoTo(l.Num, a)
|
||||
}
|
||||
st, _, err := c.Heading(l.Num)
|
||||
if err != nil || !st.Connected {
|
||||
// No reading to compare against. The plain bearing is always reachable;
|
||||
// the overlap is an optimisation, not a requirement.
|
||||
return c.GoTo(l.Num, a)
|
||||
}
|
||||
if st.LimitCW <= 360 {
|
||||
// OpsLog is set to 450 and the Genius is not. Said once per move rather
|
||||
// than silently doing the wrong thing — the fix is in the Genius's own
|
||||
// rotator configuration, and nothing here can reach past its limits.
|
||||
rotgeniusRangeWarn(l, st)
|
||||
return c.GoTo(l.Num, a)
|
||||
}
|
||||
target := a
|
||||
if alt := a + 360; alt <= st.LimitCW && absInt(alt-st.Azimuth) < absInt(a-st.Azimuth) {
|
||||
target = alt
|
||||
applog.Printf("rotator: %d° is nearer as %d° from the antenna's %d° (Genius limit %d)",
|
||||
a, alt, st.Azimuth, st.LimitCW)
|
||||
}
|
||||
return c.GoTo(l.Num, target)
|
||||
}
|
||||
|
||||
// rotgeniusRangeWarn says, at most once a minute, that the two ends disagree
|
||||
// about the mast.
|
||||
var rotgeniusWarnedAt sync.Map // host:port → time.Time
|
||||
|
||||
func rotgeniusRangeWarn(l rotorLink, st rotgenius.Status) {
|
||||
key := fmt.Sprintf("%s:%d/%d", l.Host, l.Port, l.Num)
|
||||
if v, ok := rotgeniusWarnedAt.Load(key); ok {
|
||||
if t, _ := v.(time.Time); time.Since(t) < time.Minute {
|
||||
return
|
||||
}
|
||||
}
|
||||
rotgeniusWarnedAt.Store(key, time.Now())
|
||||
applog.Printf("rotator: OpsLog is set to a 450° mast but the Rotator Genius is configured %d° to %d° — "+
|
||||
"a range it will not go past, whatever is asked. Change the limits in the Genius's own Rotator "+
|
||||
"Configuration; until then the antenna takes the long way round through north.",
|
||||
st.LimitCCW, st.LimitCW)
|
||||
}
|
||||
|
||||
func absInt(v int) int {
|
||||
if v < 0 {
|
||||
return -v
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user