fix(ultrabeam): flush stale replies (not seq-match) + instant moving flag
The antenna does NOT echo our sequence number — its replies carry their own counter — so the previous seq-matching drained every reply and stalled status updates. Revert to reading one reply per command, but flush any bytes left in the stream before each command (drainStale): a reply left by a timed-out command is discarded so the next read stays 1:1. readPacket also resyncs to the next STX. This fixes the intermittent disconnects, phantom frequency jumps and wrong element-length readings without depending on the seq. Also: report motion for a short window right after a commanded move, so the "moving" indicator and the Flex TX-inhibit fire the instant a band/pattern is clicked instead of a poll (~2 s) later; the real motor state takes over once polled.
This commit is contained in:
@@ -5,14 +5,14 @@ import (
|
||||
"bytes"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// A stale reply left in the stream by an earlier timed-out command must be
|
||||
// DRAINED, not taken for this command's reply. Without seq matching, a STATUS
|
||||
// query would return a lingering READ_BANDS/PROGRESS payload — which showed up
|
||||
// on a remote link as phantom frequencies (a spurious follow-loop re-tune) and
|
||||
// dropped connections.
|
||||
func TestSendCommandDrainsStaleReplyAndMatchesSeq(t *testing.T) {
|
||||
// sendCommand must flush any bytes already in the stream (a reply left by an
|
||||
// earlier timed-out command) before reading, then return the reply to THIS
|
||||
// command. The antenna does not echo our sequence number, so keeping the stream
|
||||
// clean is the only way to stay in sync.
|
||||
func TestSendCommandFlushesStaleThenReadsReply(t *testing.T) {
|
||||
srvConn, cliConn := net.Pipe()
|
||||
defer srvConn.Close()
|
||||
defer cliConn.Close()
|
||||
@@ -22,8 +22,14 @@ func TestSendCommandDrainsStaleReplyAndMatchesSeq(t *testing.T) {
|
||||
|
||||
errc := make(chan error, 1)
|
||||
go func() {
|
||||
// Read the request, learn its seq, then answer with a stale reply (wrong
|
||||
// seq) FIRST, then the real one.
|
||||
// Leftover from a "previous" command still sitting in the stream. The Write
|
||||
// blocks until drainStale consumes it, so it is guaranteed flushed before
|
||||
// the real exchange.
|
||||
if _, err := srvConn.Write(c.buildPacket(9, UB_OK, []byte{0xDE, 0xAD})); err != nil {
|
||||
errc <- err
|
||||
return
|
||||
}
|
||||
// Now serve the actual request.
|
||||
req, err := srv.readPacket()
|
||||
if err != nil {
|
||||
errc <- err
|
||||
@@ -34,16 +40,8 @@ func TestSendCommandDrainsStaleReplyAndMatchesSeq(t *testing.T) {
|
||||
errc <- err
|
||||
return
|
||||
}
|
||||
staleSeq := (seq + 1) % 128
|
||||
if _, err := srvConn.Write(c.buildPacket(staleSeq, UB_OK, []byte{0xAA, 0xBB})); err != nil {
|
||||
errc <- err
|
||||
return
|
||||
}
|
||||
if _, err := srvConn.Write(c.buildPacket(seq, UB_OK, []byte{0x11, 0x22, 0x33})); err != nil {
|
||||
errc <- err
|
||||
return
|
||||
}
|
||||
errc <- nil
|
||||
_, err = srvConn.Write(c.buildPacket(seq, UB_OK, []byte{0x11, 0x22, 0x33}))
|
||||
errc <- err
|
||||
}()
|
||||
|
||||
payload, err := c.sendCommand(CMD_STATUS, nil)
|
||||
@@ -51,7 +49,7 @@ func TestSendCommandDrainsStaleReplyAndMatchesSeq(t *testing.T) {
|
||||
t.Fatalf("sendCommand: %v", err)
|
||||
}
|
||||
if !bytes.Equal(payload, []byte{0x11, 0x22, 0x33}) {
|
||||
t.Fatalf("payload = % X, want 11 22 33 — stale reply not drained or seq not matched", payload)
|
||||
t.Fatalf("payload = % X, want 11 22 33 — stale reply not flushed", payload)
|
||||
}
|
||||
if err := <-errc; err != nil {
|
||||
t.Fatalf("server: %v", err)
|
||||
@@ -78,3 +76,19 @@ func TestReadPacketResyncsToSTX(t *testing.T) {
|
||||
t.Fatalf("seq=%d cmd=%d payload=% X, want 5 / OK / 01 02", seq, cmd, payload)
|
||||
}
|
||||
}
|
||||
|
||||
// A commanded move must report motion immediately (before the next status poll),
|
||||
// then fall back to the real motor state once the window elapses.
|
||||
func TestOptimisticMotionAfterCommand(t *testing.T) {
|
||||
c := &Client{lastStatus: &Status{Connected: true, MotorsMoving: 0}}
|
||||
|
||||
c.moveCmdAt = time.Now()
|
||||
if st, _ := c.GetStatus(); st.MotorsMoving == 0 {
|
||||
t.Fatal("just after a move command, GetStatus should report motion")
|
||||
}
|
||||
|
||||
c.moveCmdAt = time.Now().Add(-ubMoveOptimisticWindow - time.Second)
|
||||
if st, _ := c.GetStatus(); st.MotorsMoving != 0 {
|
||||
t.Fatal("past the window with motors idle, GetStatus must report no motion")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user