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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
46 changes: 42 additions & 4 deletions pkg/espflasher/flasher.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
21 changes: 18 additions & 3 deletions pkg/espflasher/protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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
}
Expand Down
Loading