diff --git a/go.mod b/go.mod index 6bcc2bd..ab0f92e 100644 --- a/go.mod +++ b/go.mod @@ -5,12 +5,12 @@ go 1.22 require ( github.com/stretchr/testify v1.7.0 go.bug.st/serial v1.6.2 + golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 ) require ( github.com/creack/goselect v0.1.2 // indirect github.com/davecgh/go-spew v1.1.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/sys v0.0.0-20220829200755-d48e67d00261 // indirect gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect ) diff --git a/pkg/espflasher/flasher.go b/pkg/espflasher/flasher.go index fcc3d79..a5beb15 100644 --- a/pkg/espflasher/flasher.go +++ b/pkg/espflasher/flasher.go @@ -737,8 +737,10 @@ func (f *Flasher) flashCompressed(data []byte, offset uint32, progress ProgressF blockLen := min(compSize-sent, writeSize) block := compressed[sent : sent+blockLen] - if err := f.conn.flashDeflData(block, seq); err != nil { - return fmt.Errorf("flash block %d of %d: %w", seq, numBlocks, err) + if err := f.retryFlashBlock(seq, numBlocks, func() error { + return f.conn.flashDeflData(block, seq) + }); err != nil { + return err } sent += blockLen @@ -810,8 +812,10 @@ func (f *Flasher) flashUncompressed(data []byte, offset uint32, progress Progres block = padded } - if err := f.conn.flashData(block, seq); err != nil { - return fmt.Errorf("flash block %d of %d: %w", seq, numBlocks, err) + if err := f.retryFlashBlock(seq, numBlocks, func() error { + return f.conn.flashData(block, seq) + }); err != nil { + return err } sent += blockLen @@ -923,6 +927,13 @@ func (f *Flasher) EraseRegion(offset, size uint32, progress ProgressFunc) error }) } +// flashBlockRetries is the number of attempts for each flash data block write. +// Transient serial errors (SLIP timeouts, framing glitches, USB CDC buffer +// drops) are common during flashing and typically resolve on retry. The stub +// handles duplicate sequence numbers gracefully, so resending an +// already-processed block is safe. +const flashBlockRetries = 3 + // eraseProgressInterval is the tick interval used by tickErase when reporting // synthetic erase progress via EraseFlash and EraseRegion. const eraseProgressInterval = 500 * time.Millisecond @@ -1269,6 +1280,33 @@ func compressData(data []byte) ([]byte, error) { return buf.Bytes(), nil } +// retryFlashBlock attempts a flash block write up to flashBlockRetries times. +// On failure, it flushes stale serial data and waits briefly before retrying. +// This handles transient SLIP timeouts and framing errors that are common +// during ESP32 flash operations, matching esptool.py's WRITE_BLOCK_ATTEMPTS +// retry behavior. +func (f *Flasher) retryFlashBlock(seq, numBlocks uint32, writeFn func() error) error { + var err error + for attempt := range flashBlockRetries { + err = writeFn() + if err == nil { + return nil + } + if attempt < flashBlockRetries-1 { + f.logf("Warning: block %d/%d write failed (attempt %d/%d): %v — retrying", + seq, numBlocks, attempt+1, flashBlockRetries, err) + // Wait before flushing so a delayed response from the timed-out + // attempt has time to arrive and gets cleared by the flush. + // Without this ordering, the stale response arrives after the + // flush and corrupts the retry's response parsing (e.g. SLIP + // END 0xC0 read as a status byte). + time.Sleep(200 * time.Millisecond) + f.conn.flushInput() + } + } + return fmt.Errorf("flash block %d of %d: %w", seq, numBlocks, err) +} + // logf logs a message if a logger is configured. func (f *Flasher) logf(format string, args ...interface{}) { if f.opts.Logger != nil { diff --git a/pkg/espflasher/protocol.go b/pkg/espflasher/protocol.go index dc67350..afcdea8 100644 --- a/pkg/espflasher/protocol.go +++ b/pkg/espflasher/protocol.go @@ -92,6 +92,10 @@ type conn struct { // deflCompSize is the compressed size passed to the most recent // flashDeflBegin call, used to scale the flashDeflEnd ack timeout. deflCompSize uint32 + // deflUncompSize is the uncompressed size passed to the most recent + // flashDeflBegin call, used with deflCompSize to estimate the + // decompression ratio for per-block timeouts in flashDeflData. + deflUncompSize uint32 } // isStub returns whether the stub loader is running. @@ -456,9 +460,11 @@ func (c *conn) flashDeflBegin(uncompSize, compSize, offset uint32, encrypted boo timeout := eraseTimeoutForSize(uncompSize) - // Remember the compressed size for this download so flashDeflEnd can - // scale its own ack timeout the same way. + // Remember sizes for this download so flashDeflData can estimate per-block + // decompressed sizes for timeout scaling, and flashDeflEnd can scale its + // own ack timeout. c.deflCompSize = compSize + c.deflUncompSize = uncompSize // ESP32-S2 and newer ROM bootloaders support a 5th parameter (encrypted // flag). ESP8266 and original ESP32 ROM only accept 4 parameters (16 bytes). @@ -488,7 +494,16 @@ func (c *conn) flashDeflData(block []byte, seq uint32) error { binary.LittleEndian.PutUint32(data[12:16], 0) copy(data[16:], block) - timeout := flashWriteTimeoutForSize(uint32(len(block))) + // The stub must decompress this block and write the result to flash + // before ACKing. The decompressed data can be much larger than the + // compressed block, so scale the timeout by the compression ratio + // to account for the actual flash write time. + estimatedSize := uint32(len(block)) + if c.deflCompSize > 0 && c.deflUncompSize > c.deflCompSize { + ratio := float64(c.deflUncompSize) / float64(c.deflCompSize) + estimatedSize = uint32(float64(len(block)) * ratio) + } + timeout := flashWriteTimeoutForSize(estimatedSize) _, err := c.checkCommand("write compressed flash block", cmdFlashDeflData, data, checksum(block), timeout, 0) return err }