diff --git a/internal/steppir/steppir.go b/internal/steppir/steppir.go index bf79136..9ed96df 100644 --- a/internal/steppir/steppir.go +++ b/internal/steppir/steppir.go @@ -23,6 +23,7 @@ package steppir import ( + "bytes" "encoding/binary" "fmt" "io" @@ -84,6 +85,12 @@ type Client struct { lastStatus *Status lastSetKHz int + // lastRaw holds the previous raw status frame so we only log a status line + // when the controller's reply actually changes — enough to diagnose a stuck + // "motors moving" read (which drives the app's TX-inhibit interlock) without + // spamming the log every 2 s poll. + lastRaw []byte + // A just-commanded direction is held until the controller's poll reports it — // the motors take a second or two, and a stale poll would otherwise snap the // UI back. Same trick as the Ultrabeam client. @@ -242,7 +249,17 @@ func (c *Client) queryStatus() (*Status, error) { if _, err := io.ReadFull(conn, buf); err != nil { return nil, fmt.Errorf("read status: %w", err) } - return parseStatus(buf) + st, err := parseStatus(buf) + // Log the raw frame + decode whenever it changes. The motor byte (buf[6]) is + // what decides st.MotorsMoving, and that in turn drives the app's "block TX + // while moving" interlock — so if a controller (e.g. with its INHIBIT engaged) + // reports a byte we misread as perpetual motion, this line makes it visible. + if err == nil && !bytes.Equal(buf, c.lastRaw) { + c.lastRaw = append(c.lastRaw[:0], buf...) + log.Printf("steppir: status ← % X (freq=%d kHz dir=%d moving=%d motorByte=0x%02X dirByte=0x%02X)", + buf, st.Frequency, st.Direction, st.MotorsMoving, buf[6], buf[7]) + } + return st, err } // parseStatus decodes an 11-byte status frame.