Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 14 additions & 3 deletions pkg/espflasher/flasher.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)"}
}
Expand All @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions pkg/espflasher/flasher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down
3 changes: 2 additions & 1 deletion pkg/espflasher/protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
42 changes: 42 additions & 0 deletions pkg/espflasher/protocol_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
5 changes: 5 additions & 0 deletions pkg/espflasher/slip.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
20 changes: 20 additions & 0 deletions pkg/espflasher/slip_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
8 changes: 6 additions & 2 deletions tools/update-stubs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
Loading