Files
OpsLog/internal/ultrabeam/ultrabeam_test.go
T
rouggy 821883dfd3 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.
2026-08-05 00:13:53 +02:00

95 lines
2.9 KiB
Go

package ultrabeam
import (
"bufio"
"bytes"
"net"
"testing"
"time"
)
// 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()
c := &Client{conn: cliConn, reader: bufio.NewReader(cliConn)}
srv := &Client{reader: bufio.NewReader(srvConn)}
errc := make(chan error, 1)
go func() {
// 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
return
}
seq, _, _, err := parsePacket(req)
if err != nil {
errc <- err
return
}
_, err = srvConn.Write(c.buildPacket(seq, UB_OK, []byte{0x11, 0x22, 0x33}))
errc <- err
}()
payload, err := c.sendCommand(CMD_STATUS, nil)
if err != nil {
t.Fatalf("sendCommand: %v", err)
}
if !bytes.Equal(payload, []byte{0x11, 0x22, 0x33}) {
t.Fatalf("payload = % X, want 11 22 33 — stale reply not flushed", payload)
}
if err := <-errc; err != nil {
t.Fatalf("server: %v", err)
}
}
// readPacket must resynchronise to the next STX, dropping any partial/garbage
// bytes left in the stream, so one corrupt frame can't misalign every frame
// after it.
func TestReadPacketResyncsToSTX(t *testing.T) {
frame := (&Client{}).buildPacket(5, UB_OK, []byte{0x01, 0x02})
stream := append([]byte{0x11, 0x22, 0x33}, frame...) // leading garbage, then a real frame
c := &Client{reader: bufio.NewReader(bytes.NewReader(stream))}
got, err := c.readPacket()
if err != nil {
t.Fatalf("readPacket: %v", err)
}
seq, cmd, payload, err := parsePacket(got)
if err != nil {
t.Fatalf("parsePacket: %v", err)
}
if seq != 5 || cmd != UB_OK || !bytes.Equal(payload, []byte{0x01, 0x02}) {
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")
}
}