fix: drain SPE status backlog so display is current (was ~15s stale); ON now pulses DTR (0x0B is level-only); power-level cycle waits for real status change

This commit is contained in:
2026-07-21 01:38:39 +02:00
parent 51c4bda71a
commit 2194279602
+62 -8
View File
@@ -146,30 +146,53 @@ func (c *Client) Operate(on bool) error {
// ToggleOperate flips STANDBY/OPERATE unconditionally. // ToggleOperate flips STANDBY/OPERATE unconditionally.
func (c *Client) ToggleOperate() error { return c.sendCmd(cmdOperate) } func (c *Client) ToggleOperate() error { return c.sendCmd(cmdOperate) }
// PowerOn presses the POWER key (turns the amp on when it's off). // PowerOn turns the amplifier on. Per the SPE manual this is NOT a data command
func (c *Client) PowerOn() error { return c.sendCmd(cmdPower) } // but a hardware DTR pulse on the serial line (the "Remote_ON" pin), which needs
// the special SPE cable — so it only applies to the serial transport. Over TCP
// there is no DTR line and power-on isn't possible remotely.
func (c *Client) PowerOn() error {
c.mu.Lock()
defer c.mu.Unlock()
sp, ok := c.conn.(serial.Port)
if !ok {
return fmt.Errorf("power-on needs the serial DTR line (special SPE cable); not available over network")
}
// A single positive pulse (~1.2s) on DTR, as the manual describes.
if err := sp.SetDTR(true); err != nil {
return err
}
time.Sleep(1200 * time.Millisecond)
return sp.SetDTR(false)
}
// PowerOff presses the OFF key (switches the amp off). // PowerOff presses the OFF key (switches the amp off).
func (c *Client) PowerOff() error { return c.sendCmd(cmdOff) } func (c *Client) PowerOff() error { return c.sendCmd(cmdOff) }
// SetPowerLevel cycles the output power level (L/M/H) to the requested one by // SetPowerLevel cycles the output power level (L/M/H) to the requested one by
// tapping the POWER key until the reported level matches, giving the amp time to // tapping the POWER key (0x0B) and WAITING for the amp to actually report the new
// report each change. `level` is "L", "M" or "H" (case-insensitive). // level before the next tap — the status is streamed, so we poll GetStatus rather
// than sleeping a fixed time. `level` is "L", "M" or "H" (case-insensitive).
func (c *Client) SetPowerLevel(level string) error { func (c *Client) SetPowerLevel(level string) error {
want := strings.ToUpper(strings.TrimSpace(level)) want := strings.ToUpper(strings.TrimSpace(level))
if want == "" { if want == "" {
return nil return nil
} }
// At most 3 taps to walk the 3-way cycle back around to the target. // At most 3 taps to walk the 3-way L→M→H cycle around to the target.
for i := 0; i < 3; i++ { for i := 0; i < 3; i++ {
cur := strings.ToUpper(strings.TrimSpace(c.GetStatus().PowerLevel)) if strings.ToUpper(strings.TrimSpace(c.GetStatus().PowerLevel)) == want {
if cur == want {
return nil return nil
} }
prev := strings.ToUpper(strings.TrimSpace(c.GetStatus().PowerLevel))
if err := c.sendCmd(cmdPower); err != nil { if err := c.sendCmd(cmdPower); err != nil {
return err return err
} }
time.Sleep(1 * time.Second) // let the amp apply it and the poll refresh status // Wait (up to ~2s) for the streamed status to reflect the change.
for w := 0; w < 20; w++ {
time.Sleep(100 * time.Millisecond)
if strings.ToUpper(strings.TrimSpace(c.GetStatus().PowerLevel)) != prev {
break
}
}
} }
return nil return nil
} }
@@ -186,6 +209,11 @@ func (c *Client) pollLoop() {
c.setErr(fmt.Errorf("connect: %w", err)) c.setErr(fmt.Errorf("connect: %w", err))
continue continue
} }
// The amp streams status frames faster than one per poll, so a backlog
// builds up and we'd read stale frames (the display lagged ~15s behind
// the real amp). Flush anything pending, THEN request + read exactly one
// fresh frame — the status we show is always current.
c.drainInput()
if err := c.sendCmd(cmdStatus); err != nil { if err := c.sendCmd(cmdStatus); err != nil {
c.mu.Lock() c.mu.Lock()
c.dropLocked() c.dropLocked()
@@ -229,6 +257,32 @@ func (c *Client) dropLocked() {
} }
} }
// drainInput discards everything currently pending on the link (OS buffer + the
// bufio reader) so the next read returns a fresh frame rather than a queued stale
// one. Called before each status request.
func (c *Client) drainInput() {
c.mu.Lock()
defer c.mu.Unlock()
if c.conn == nil {
return
}
if sp, ok := c.conn.(serial.Port); ok {
_ = sp.ResetInputBuffer()
} else if nc, ok := c.conn.(net.Conn); ok {
_ = nc.SetReadDeadline(time.Now().Add(15 * time.Millisecond))
buf := make([]byte, 4096)
for {
n, err := nc.Read(buf)
if n == 0 || err != nil {
break
}
}
}
if c.r != nil {
c.r.Reset(c.conn)
}
}
// sendCmd frames one keystroke code and writes it. Single-byte payload → CHK is // sendCmd frames one keystroke code and writes it. Single-byte payload → CHK is
// the code itself. // the code itself.
func (c *Client) sendCmd(code byte) error { func (c *Client) sendCmd(code byte) error {