From 2194279602abfddc476dbe8bc6fd71934ec9369b Mon Sep 17 00:00:00 2001 From: rouggy Date: Tue, 21 Jul 2026 01:38:39 +0200 Subject: [PATCH] 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 --- internal/spe/spe.go | 70 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 62 insertions(+), 8 deletions(-) diff --git a/internal/spe/spe.go b/internal/spe/spe.go index 9212816..8e7b64f 100644 --- a/internal/spe/spe.go +++ b/internal/spe/spe.go @@ -146,30 +146,53 @@ func (c *Client) Operate(on bool) error { // ToggleOperate flips STANDBY/OPERATE unconditionally. func (c *Client) ToggleOperate() error { return c.sendCmd(cmdOperate) } -// PowerOn presses the POWER key (turns the amp on when it's off). -func (c *Client) PowerOn() error { return c.sendCmd(cmdPower) } +// PowerOn turns the amplifier on. Per the SPE manual this is NOT a data command +// 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). func (c *Client) PowerOff() error { return c.sendCmd(cmdOff) } // 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 -// report each change. `level` is "L", "M" or "H" (case-insensitive). +// tapping the POWER key (0x0B) and WAITING for the amp to actually report the new +// 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 { want := strings.ToUpper(strings.TrimSpace(level)) if want == "" { 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++ { - cur := strings.ToUpper(strings.TrimSpace(c.GetStatus().PowerLevel)) - if cur == want { + if strings.ToUpper(strings.TrimSpace(c.GetStatus().PowerLevel)) == want { return nil } + prev := strings.ToUpper(strings.TrimSpace(c.GetStatus().PowerLevel)) if err := c.sendCmd(cmdPower); err != nil { 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 } @@ -186,6 +209,11 @@ func (c *Client) pollLoop() { c.setErr(fmt.Errorf("connect: %w", err)) 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 { c.mu.Lock() 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 // the code itself. func (c *Client) sendCmd(code byte) error {