diff --git a/pkg/espflasher/flasher.go b/pkg/espflasher/flasher.go index 32ea28a..1280a65 100644 --- a/pkg/espflasher/flasher.go +++ b/pkg/espflasher/flasher.go @@ -178,6 +178,12 @@ func (f *Flasher) Close() error { return f.port.Close() } +// FlushInput discards any unread data from the serial port and SLIP reader. +// Useful when reusing a flasher connection across multiple operations. +func (f *Flasher) FlushInput() { + f.conn.flushInput() +} + // reopenPort closes and reopens the serial port after a USB device // re-enumeration. TinyUSB CDC devices may briefly disappear during reset. func (f *Flasher) reopenPort() error { @@ -799,9 +805,9 @@ func (f *Flasher) GetSecurityInfo() (*SecurityInfo, error) { return f.readSecurityInfo() } -// GetMD5 returns the MD5 hash of a flash region. +// GetFlashMD5 returns the MD5 hash of a flash region. // Requires the stub loader to be running. -func (f *Flasher) GetMD5(offset, size uint32) (string, error) { +func (f *Flasher) GetFlashMD5(offset, size uint32) (string, error) { if !f.conn.isStub() { return "", &UnsupportedCommandError{Command: "flash MD5 (requires stub)"} } @@ -828,7 +834,12 @@ func (f *Flasher) ReadFlash(offset, size uint32) ([]byte, error) { return nil, err } - return f.conn.readFlash(offset, size) + data, err := f.conn.readFlash(offset, size) + // Clear any stale data left in the serial buffer and SLIP reader + // after the raw block-read protocol. Without this, leftover bytes + // can corrupt subsequent command responses. + f.conn.flushInput() + return data, err } // Reset performs a hard reset of the device, causing it to run user code. diff --git a/pkg/espflasher/flasher_test.go b/pkg/espflasher/flasher_test.go index 99b9dd0..e344471 100644 --- a/pkg/espflasher/flasher_test.go +++ b/pkg/espflasher/flasher_test.go @@ -434,11 +434,11 @@ func TestFlashSizeFromJEDECMatchesChipSizes(t *testing.T) { } } -func TestGetMD5RequiresStub(t *testing.T) { +func TestFlashMD5RequiresStub(t *testing.T) { mock := &mockConnection{} mock.stubMode = false // ROM mode f := &Flasher{conn: mock, chip: chipDefs[ChipESP32]} - _, err := f.GetMD5(0, 1024) + _, err := f.GetFlashMD5(0, 1024) if err == nil { t.Fatal("expected error when stub is not running") } diff --git a/pkg/espflasher/protocol.go b/pkg/espflasher/protocol.go index 1c25e81..e1296bb 100644 --- a/pkg/espflasher/protocol.go +++ b/pkg/espflasher/protocol.go @@ -630,9 +630,10 @@ func (c *conn) flashWriteSize() uint32 { return flashWriteSizeROM } -// flushInput discards any unread data from the serial port. +// flushInput discards any unread data from the serial port and SLIP reader. func (c *conn) flushInput() { c.port.ResetInputBuffer() //nolint:errcheck + c.reader.reset() } // eraseTimeoutForSize calculates an appropriate timeout for erase operations. diff --git a/pkg/espflasher/protocol_test.go b/pkg/espflasher/protocol_test.go index b2cd3cd..dcef2d3 100644 --- a/pkg/espflasher/protocol_test.go +++ b/pkg/espflasher/protocol_test.go @@ -737,3 +737,45 @@ func TestReadFlashParameterValidation(t *testing.T) { t.Errorf("cmdReadFlash opcode mismatch") } } + +// flushTrackingPort embeds mockPort and records ResetInputBuffer calls. +type flushTrackingPort struct { + mockPort + resetCount int +} + +func (f *flushTrackingPort) ResetInputBuffer() error { + f.resetCount++ + return nil +} + +func TestConnFlushInputResetsPortAndReader(t *testing.T) { + port := &flushTrackingPort{} + c := newConn(port) + c.reader.leftover = []byte{0xDE, 0xAD, 0xBE, 0xEF} + + c.flushInput() + + if port.resetCount != 1 { + t.Errorf("ResetInputBuffer called %d times, want 1", port.resetCount) + } + if c.reader.leftover != nil { + t.Errorf("reader.leftover = %X after flushInput, want nil", c.reader.leftover) + } +} + +func TestFlasherFlushInputDelegatesToConn(t *testing.T) { + port := &flushTrackingPort{} + c := newConn(port) + c.reader.leftover = []byte{0x01, 0x02} + f := &Flasher{conn: c} + + f.FlushInput() + + if port.resetCount != 1 { + t.Errorf("ResetInputBuffer called %d times, want 1", port.resetCount) + } + if c.reader.leftover != nil { + t.Errorf("reader.leftover not cleared by Flasher.FlushInput") + } +} diff --git a/pkg/espflasher/slip.go b/pkg/espflasher/slip.go index b991df6..0a7b367 100644 --- a/pkg/espflasher/slip.go +++ b/pkg/espflasher/slip.go @@ -77,6 +77,11 @@ func newSlipReader(port serial.Port) *slipReader { return &slipReader{port: port} } +// reset clears any buffered data from the SLIP reader. +func (r *slipReader) reset() { + r.leftover = nil +} + // ReadFrame reads a single SLIP-framed packet from the serial port. // It blocks until a complete frame is received or the timeout expires. func (r *slipReader) ReadFrame(timeout time.Duration) ([]byte, error) { diff --git a/pkg/espflasher/slip_test.go b/pkg/espflasher/slip_test.go index 948837e..b1f0299 100644 --- a/pkg/espflasher/slip_test.go +++ b/pkg/espflasher/slip_test.go @@ -155,3 +155,23 @@ t.Errorf("raw 0xC0 found at inner byte %d in encoded data %X", i, encoded) } } } + +func TestSlipReaderResetClearsLeftover(t *testing.T) { + r := newSlipReader(nil) + r.leftover = []byte{0xDE, 0xAD, 0xBE, 0xEF} + + r.reset() + + if r.leftover != nil { + t.Errorf("leftover after reset = %X, want nil", r.leftover) + } +} + +func TestSlipReaderResetIsIdempotent(t *testing.T) { + r := newSlipReader(nil) + r.reset() + r.reset() + if r.leftover != nil { + t.Errorf("leftover after double reset = %X, want nil", r.leftover) + } +} diff --git a/tools/update-stubs.go b/tools/update-stubs.go index ccde755..7e42fe5 100644 --- a/tools/update-stubs.go +++ b/tools/update-stubs.go @@ -54,7 +54,9 @@ func download(url, dest string) error { if err != nil { return err } - defer resp.Body.Close() + defer func() { + _ = resp.Body.Close() + }() if resp.StatusCode != http.StatusOK { return fmt.Errorf("HTTP %s for %s", resp.Status, url) @@ -64,7 +66,9 @@ func download(url, dest string) error { if err != nil { return err } - defer f.Close() + defer func() { + _ = f.Close() + }() _, err = io.Copy(f, resp.Body) return err