chore: release v0.21.3
This commit is contained in:
@@ -1,8 +1,12 @@
|
||||
package steppir
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// The exact bytes are the correctness checksum. If buildSet ever drifts from the
|
||||
@@ -104,3 +108,120 @@ func TestParseStatus(t *testing.T) {
|
||||
t.Error("active-motors 0xFF (command received) must not read as moving")
|
||||
}
|
||||
}
|
||||
|
||||
// fakeConn stands in for the controller link. rx holds bytes the "controller"
|
||||
// has already sent (what the client will read), tx collects what the client
|
||||
// wrote, and a "?A" query queues `reply` into rx the way the SDA answers.
|
||||
//
|
||||
// Reads never block: an empty rx returns (0, nil), which is exactly how
|
||||
// go.bug.st/serial reports a read timeout, so drain() sees the same
|
||||
// nothing-left signal it gets from real hardware.
|
||||
type fakeConn struct {
|
||||
mu sync.Mutex
|
||||
rx bytes.Buffer
|
||||
tx bytes.Buffer
|
||||
reply []byte
|
||||
}
|
||||
|
||||
func (f *fakeConn) Read(p []byte) (int, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
if f.rx.Len() == 0 {
|
||||
return 0, nil
|
||||
}
|
||||
return f.rx.Read(p)
|
||||
}
|
||||
|
||||
func (f *fakeConn) Write(p []byte) (int, error) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.tx.Write(p)
|
||||
if bytes.Contains(p, []byte("?A")) {
|
||||
f.rx.Write(f.reply)
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (f *fakeConn) Close() error { return nil }
|
||||
|
||||
var (
|
||||
// 50.150 MHz, normal — the "stuck at 6 m" frame that kept turning up in
|
||||
// F4BPO's friend's log long after the rig had left the band.
|
||||
frame6mNormal = []byte{0x40, 0x41, 0x00, 0x4C, 0x85, 0xD8, 0x00, 0x07, 0x30, 0x38, 0x0D}
|
||||
// 14.250 MHz, 180° — what the controller actually reports right now.
|
||||
frame20m180 = []byte{0x40, 0x41, 0x00, 0x15, 0xBE, 0x68, 0x00, 0x47, 0x30, 0x38, 0x0D}
|
||||
)
|
||||
|
||||
// The controller pushes status frames unsolicited, so they pile up between polls.
|
||||
// Reading one frame per poll then returns state from minutes ago — which is how a
|
||||
// 180° command could take ~40 s to show up in the UI, and how two polls 4 s apart
|
||||
// reported 28 MHz then 14 MHz. queryStatus must empty the backlog first.
|
||||
func TestQueryStatusDiscardsQueuedFrames(t *testing.T) {
|
||||
fc := &fakeConn{reply: frame20m180}
|
||||
fc.rx.Write(frame6mNormal) // two frames the controller pushed on its own
|
||||
fc.rx.Write(frame6mNormal)
|
||||
|
||||
c := &Client{conn: fc}
|
||||
st, err := c.queryStatus()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if st.Frequency != 14250 {
|
||||
t.Errorf("freq = %d kHz, want 14250 — a stale queued frame was read instead of this poll's reply", st.Frequency)
|
||||
}
|
||||
if st.Direction != Dir180 {
|
||||
t.Errorf("direction = %d, want %d (180°)", st.Direction, Dir180)
|
||||
}
|
||||
}
|
||||
|
||||
// A reply we land on mid-frame must be rejected, not decoded into a bogus
|
||||
// frequency and direction the follow loop would then act on — and it must not
|
||||
// look like a dead link either (errBadFrame keeps the connection).
|
||||
func TestQueryStatusRejectsMalformedFrame(t *testing.T) {
|
||||
shifted := append(append([]byte{}, frame20m180[3:]...), 0x40, 0x41, 0x00) // 11 bytes, wrong header
|
||||
c := &Client{conn: &fakeConn{reply: shifted}}
|
||||
if _, err := c.queryStatus(); !errors.Is(err, errBadFrame) {
|
||||
t.Fatalf("err = %v, want errBadFrame", err)
|
||||
}
|
||||
}
|
||||
|
||||
// The direction the operator just commanded is shown until the controller
|
||||
// confirms it. The hold used to be 4 s — two polls — so the button snapped back
|
||||
// to Normal while the elements were still swapping over to 180°.
|
||||
func TestApplyPendingDirHold(t *testing.T) {
|
||||
c := &Client{pendingDir: Dir180, pendingDirAt: time.Now(), pendingDirSet: true}
|
||||
|
||||
// Controller still reports the old pattern: keep showing what was commanded.
|
||||
st := &Status{Direction: DirNormal}
|
||||
c.applyPendingDir(st)
|
||||
if st.Direction != Dir180 {
|
||||
t.Fatalf("direction = %d, want %d while the move is pending", st.Direction, Dir180)
|
||||
}
|
||||
if !c.pendingDirSet {
|
||||
t.Fatal("hold released before the controller confirmed")
|
||||
}
|
||||
|
||||
// Still holding well past the old 4 s window — a SteppIR takes longer than
|
||||
// that to report a pattern change.
|
||||
c.pendingDirAt = time.Now().Add(-10 * time.Second)
|
||||
st = &Status{Direction: DirNormal}
|
||||
c.applyPendingDir(st)
|
||||
if st.Direction != Dir180 {
|
||||
t.Fatalf("direction = %d after 10 s, want %d — the hold expired too early", st.Direction, Dir180)
|
||||
}
|
||||
|
||||
// Controller confirms: release the hold and trust its reports again.
|
||||
st = &Status{Direction: Dir180}
|
||||
c.applyPendingDir(st)
|
||||
if c.pendingDirSet {
|
||||
t.Fatal("hold should be released once the controller reports the commanded direction")
|
||||
}
|
||||
|
||||
// A command the controller never acted on must not lie forever.
|
||||
c.pendingDir, c.pendingDirAt, c.pendingDirSet = DirBi, time.Now().Add(-pendingDirTTL-time.Second), true
|
||||
st = &Status{Direction: DirNormal}
|
||||
c.applyPendingDir(st)
|
||||
if st.Direction != DirNormal || c.pendingDirSet {
|
||||
t.Fatalf("expired hold should fall back to the controller: direction = %d, pending = %v", st.Direction, c.pendingDirSet)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user