An operator reported that retracting a SteppIR shows nothing, while an Ultrabeam visibly does. The cause is not the display: both antennas feed one `moving` flag and the widget renders it the same way. moveCmdAt — the bridge that reports motion from the moment a move is COMMANDED, before the controller's own poll can confirm it — was set only by SetFrequency. Retract and Calibrate write their frame directly and armed nothing, so three things were missing at once: nothing said the elements were moving, the poll stayed on its two-second idle cadence instead of speeding up to 250 ms to watch, and the transmit inhibit was not engaged while the elements travelled. That last one is the reason this is not cosmetic. The Ultrabeam's Retract armed nothing either. It was less visible there because that controller reports element lengths, which count down on screen; a SteppIR reports none at all, so the retract looked inert. The inhibit was equally absent. The window becomes a deadline rather than a timestamp, so the caller can say how long a bridge it needs: three seconds for a tune, ten for a retract or a calibrate. Those are tens of seconds of travel, and a retract drops the controller out of AUTOTRACK where its motor-bit reporting is less predictable. Still bounded — an antenna that never reports motion must not latch the inhibit on for ever — and markMoving only ever extends, so a tune issued just after a retract cannot cut the retract's bridge down to its own. Four tests, including that a command which failed to reach the controller does not claim the antenna is moving.
101 lines
3.5 KiB
Go
101 lines
3.5 KiB
Go
package steppir
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// The controller says nothing until its own poll comes round, so a move has to
|
|
// be reported from the moment it is COMMANDED. Without it the app's "block TX
|
|
// while the elements travel" had a hole: the grace covering the command ran out
|
|
// before the first poll that would have seen the movement, and the transmitter
|
|
// was released in the middle of a move.
|
|
func TestAJustCommandedMoveReportsMotion(t *testing.T) {
|
|
c := &Client{}
|
|
c.lastStatus = &Status{Connected: true} // idle, as the controller last said
|
|
|
|
if st, _ := c.GetStatus(); st.MotorsMoving != 0 {
|
|
t.Fatal("an idle antenna reports motion")
|
|
}
|
|
|
|
c.statusMu.Lock()
|
|
c.moveUntil = time.Now().Add(moveOptimisticWindow)
|
|
c.statusMu.Unlock()
|
|
|
|
st, _ := c.GetStatus()
|
|
if st.MotorsMoving == 0 {
|
|
t.Error("a move commanded a moment ago is not reported as motion")
|
|
}
|
|
if c.lastStatus.MotorsMoving != 0 {
|
|
t.Error("the optimistic flag leaked into the cached status the poll owns")
|
|
}
|
|
|
|
// Bounded: an antenna that never reports motion must not latch the transmit
|
|
// inhibit on for ever.
|
|
c.statusMu.Lock()
|
|
c.moveUntil = time.Now().Add(-time.Second)
|
|
c.statusMu.Unlock()
|
|
if st, _ := c.GetStatus(); st.MotorsMoving != 0 {
|
|
t.Error("the optimistic window never expires")
|
|
}
|
|
}
|
|
|
|
// Retract and Calibrate move every element, and they were the two commands that
|
|
// reported nothing at all: no "moving" on screen, the poll left on its
|
|
// two-second idle cadence instead of speeding up to watch, and no transmit
|
|
// inhibit while the elements travelled. Reported by an operator whose retract
|
|
// looked inert next to an Ultrabeam's.
|
|
func TestRetractAndCalibrateReportMotion(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
call func(*Client) error
|
|
}{
|
|
{"retract", (*Client).Retract},
|
|
{"calibrate", (*Client).Calibrate},
|
|
} {
|
|
c := &Client{}
|
|
c.lastStatus = &Status{Connected: true, Frequency: 14074}
|
|
// No connection, so the write fails and the command returns an error —
|
|
// which is the point: a command that did NOT reach the controller must
|
|
// not claim the antenna is moving.
|
|
if err := tc.call(c); err == nil {
|
|
t.Fatalf("%s: expected an error with no connection", tc.name)
|
|
}
|
|
if st, _ := c.GetStatus(); st.MotorsMoving != 0 {
|
|
t.Errorf("%s: a command that failed to send reports motion", tc.name)
|
|
}
|
|
|
|
// And with the write accepted, motion is reported at once.
|
|
c.markMoving(retractOptimisticWindow)
|
|
if st, _ := c.GetStatus(); st.MotorsMoving == 0 {
|
|
t.Errorf("%s: a commanded move is not reported as motion", tc.name)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The retract bridge is longer than the tune bridge: winding every element into
|
|
// its hub takes tens of seconds, and a retract drops the controller out of
|
|
// AUTOTRACK, where its motor-bit reporting is less predictable.
|
|
func TestRetractBridgeOutlastsTheTuneBridge(t *testing.T) {
|
|
if retractOptimisticWindow <= moveOptimisticWindow {
|
|
t.Errorf("retract window %v is not longer than the tune window %v",
|
|
retractOptimisticWindow, moveOptimisticWindow)
|
|
}
|
|
// Bounded all the same — see the note on the constant.
|
|
if retractOptimisticWindow > time.Minute {
|
|
t.Errorf("retract window %v could latch the transmit inhibit on", retractOptimisticWindow)
|
|
}
|
|
}
|
|
|
|
// markMoving extends, never shortens: a tune issued a moment after a retract
|
|
// must not cut the retract's bridge down to the tune's.
|
|
func TestMarkMovingOnlyExtends(t *testing.T) {
|
|
c := &Client{}
|
|
c.markMoving(retractOptimisticWindow)
|
|
far := c.moveUntil
|
|
c.markMoving(moveOptimisticWindow)
|
|
if c.moveUntil.Before(far) {
|
|
t.Error("a shorter bridge shortened a longer one")
|
|
}
|
|
}
|