fix: bug on steppir control
This commit is contained in:
+26
-20
@@ -74,6 +74,12 @@ type Client struct {
|
||||
connMu sync.Mutex
|
||||
conn io.ReadWriteCloser
|
||||
|
||||
// ioMu serialises EVERY exchange on the shared connection — a status query
|
||||
// (write "?A" then read 11 bytes) and a command write must never interleave,
|
||||
// or their bytes mix on the wire and both frames are corrupted. The status
|
||||
// poll runs on one goroutine, tuning on another, so this is essential.
|
||||
ioMu sync.Mutex
|
||||
|
||||
statusMu sync.RWMutex
|
||||
lastStatus *Status
|
||||
lastSetKHz int
|
||||
@@ -85,11 +91,6 @@ type Client struct {
|
||||
pendingDirAt time.Time
|
||||
pendingDirSet bool
|
||||
|
||||
// After a Home/Retract the controller drops out of AUTOTRACK and ignores
|
||||
// frequency sets until it is turned back ON. Set on Retract, cleared by
|
||||
// re-enabling on the next SetFrequency.
|
||||
needAutotrack bool
|
||||
|
||||
stopChan chan struct{}
|
||||
running bool
|
||||
}
|
||||
@@ -231,6 +232,8 @@ func (c *Client) queryStatus() (*Status, error) {
|
||||
if conn == nil {
|
||||
return nil, fmt.Errorf("steppir: not connected")
|
||||
}
|
||||
c.ioMu.Lock()
|
||||
defer c.ioMu.Unlock()
|
||||
setDeadline(conn, 3*time.Second)
|
||||
if _, err := conn.Write([]byte("?A\r")); err != nil {
|
||||
return nil, fmt.Errorf("write status cmd: %w", err)
|
||||
@@ -299,6 +302,9 @@ func (c *Client) writeCmd(pkt []byte) error {
|
||||
if conn == nil {
|
||||
return fmt.Errorf("steppir: not connected")
|
||||
}
|
||||
c.ioMu.Lock()
|
||||
defer c.ioMu.Unlock()
|
||||
log.Printf("steppir: → % X", pkt)
|
||||
setDeadline(conn, 3*time.Second)
|
||||
if _, err := conn.Write(pkt); err != nil {
|
||||
c.closeConn()
|
||||
@@ -309,16 +315,19 @@ func (c *Client) writeCmd(pkt []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetFrequency tunes the elements to freqKhz with the given direction. If a prior
|
||||
// Retract dropped AUTOTRACK, re-enable it first — otherwise the set is ignored.
|
||||
// SetFrequency tunes the elements to freqKhz with the given direction.
|
||||
//
|
||||
// AUTOTRACK is (re-)enabled first, EVERY time: the controller ignores frequency
|
||||
// sets unless it is in AUTOTRACK mode ("when not in AUTOTRACK only CALIBRATE and
|
||||
// RETRACT work"), and it can be out of AUTOTRACK at power-on, after a Home, or if
|
||||
// switched off on the front panel. Sending the 'R' command each tune is cheap and
|
||||
// makes tuning work regardless of the controller's current mode — which is what
|
||||
// was silently failing before.
|
||||
func (c *Client) SetFrequency(freqKhz int, direction int) error {
|
||||
if c.needAutotrack {
|
||||
if err := c.writeCmd(buildSet(freqKhz*1000, direction, 'R')); err != nil {
|
||||
return err
|
||||
}
|
||||
c.needAutotrack = false
|
||||
if err := c.writeCmd(buildSet(freqKhz*1000, direction, 'R')); err != nil { // AUTOTRACK ON
|
||||
return err
|
||||
}
|
||||
if err := c.writeCmd(buildSet(freqKhz*1000, direction, '1')); err != nil {
|
||||
if err := c.writeCmd(buildSet(freqKhz*1000, direction, '1')); err != nil { // set freq + dir
|
||||
return err
|
||||
}
|
||||
c.statusMu.Lock()
|
||||
@@ -343,8 +352,9 @@ func (c *Client) SetDirection(direction int) error {
|
||||
return c.SetFrequency(khz, direction)
|
||||
}
|
||||
|
||||
// Retract homes the elements into the hubs (storage). This leaves AUTOTRACK off,
|
||||
// so the next SetFrequency re-enables it.
|
||||
// Retract homes the elements into the hubs (storage). This drops the controller
|
||||
// out of AUTOTRACK, but that is handled transparently: the next SetFrequency
|
||||
// re-issues AUTOTRACK ON before tuning.
|
||||
func (c *Client) Retract() error {
|
||||
// A valid frequency must accompany the command; reuse the last one.
|
||||
khz := c.LastSetKHz()
|
||||
@@ -355,9 +365,5 @@ func (c *Client) Retract() error {
|
||||
khz = 14000 // any in-range value; the controller just homes
|
||||
}
|
||||
}
|
||||
if err := c.writeCmd(buildSet(khz*1000, DirNormal, 'S')); err != nil {
|
||||
return err
|
||||
}
|
||||
c.needAutotrack = true
|
||||
return nil
|
||||
return c.writeCmd(buildSet(khz*1000, DirNormal, 'S'))
|
||||
}
|
||||
|
||||
@@ -50,10 +50,32 @@ func TestBuildSetDirectionAndCommand(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// A REAL status frame captured off an SDA controller (F4BPO's, 2026-07-15):
|
||||
// 40 41 00 4C C5 84 00 87 30 38 0D — 50.313 MHz, idle, bidirectional, interface
|
||||
// version "08". Using the actual device output (rather than hand-built bytes)
|
||||
// pins parseStatus to real hardware, and independently confirms the ÷10 wire
|
||||
// scale: 0x4CC584 × 10 = 50 313 000 Hz, a genuine 6 m frequency.
|
||||
func TestParseStatusRealFrame(t *testing.T) {
|
||||
frame := []byte{0x40, 0x41, 0x00, 0x4C, 0xC5, 0x84, 0x00, 0x87, 0x30, 0x38, 0x0D}
|
||||
st, err := parseStatus(frame)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if st.Frequency != 50313 {
|
||||
t.Errorf("freq = %d kHz, want 50313 (50.313 MHz)", st.Frequency)
|
||||
}
|
||||
if st.Direction != DirBi { // 0x87 & 0xE0 = 0x80 = bidirectional
|
||||
t.Errorf("direction = %d, want %d (bidirectional)", st.Direction, DirBi)
|
||||
}
|
||||
if st.MotorsMoving != 0 {
|
||||
t.Errorf("moving = %d, want 0 (motors byte 0x00)", st.MotorsMoving)
|
||||
}
|
||||
}
|
||||
|
||||
// parseStatus decodes what the controller sends back — the inverse of buildSet's
|
||||
// frequency field, plus the direction nibble.
|
||||
// frequency field, plus the direction nibble and the motors-busy byte.
|
||||
func TestParseStatus(t *testing.T) {
|
||||
frame := []byte{0x00, 0x00, 0x00, 0x15, 0x79, 0xA8, 0x01, 0x40, '1', '2', 0x0D}
|
||||
frame := []byte{0x40, 0x41, 0x00, 0x15, 0x79, 0xA8, 0x01, 0x40, '0', '8', 0x0D}
|
||||
st, err := parseStatus(frame)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
Reference in New Issue
Block a user