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:
+62
-8
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user