Skip to content
Closed
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
5 changes: 5 additions & 0 deletions pkg/espflasher/chip.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,11 @@ type chipDef struct {

// FlashSizes maps size strings to header byte values.
FlashSizes map[string]byte

// PostConnect is called after chip detection to perform chip-specific
// initialization (e.g. USB interface detection, watchdog disable).
// May set Flasher fields like usesUSB.
PostConnect func(f *Flasher) error
}

// chipDetectMagicRegAddr is the register address that has a different
Expand Down
73 changes: 66 additions & 7 deletions pkg/espflasher/flasher.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,20 +105,23 @@ type connection interface {
changeBaud(newBaud, oldBaud uint32) error
eraseFlash() error
eraseRegion(offset, size uint32) error
readFlash(offset, size uint32) ([]byte, error)
flushInput()
isStub() bool
setUSB(v bool)
setSupportsEncryptedFlash(v bool)
loadStub(s *stub) error
}

// Flasher manages the connection to an ESP device and provides
// high-level flash operations.
type Flasher struct {
port serial.Port
conn connection
chip *chipDef
opts *FlasherOptions
portStr string
port serial.Port
conn connection
chip *chipDef
opts *FlasherOptions
portStr string
usesUSB bool
}

// New creates a new Flasher connected to the given serial port.
Expand Down Expand Up @@ -162,7 +165,7 @@ func New(portName string, opts *FlasherOptions) (*Flasher, error) {

// Connect to the bootloader
if err := f.connect(); err != nil {
port.Close() //nolint:errcheck
f.port.Close() //nolint:errcheck
return nil, err
}

Expand All @@ -174,6 +177,31 @@ func (f *Flasher) Close() error {
return f.port.Close()
}

// 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 {
f.port.Close() //nolint:errcheck

var lastErr error
deadline := time.Now().Add(3 * time.Second)
for time.Now().Before(deadline) {
time.Sleep(500 * time.Millisecond)
port, err := serial.Open(f.portStr, &serial.Mode{
BaudRate: f.opts.BaudRate,
Parity: serial.NoParity,
DataBits: 8,
StopBits: serial.OneStopBit,
})
if err == nil {
f.port = port
f.conn = newConn(port)
return nil
}
lastErr = err
}
return fmt.Errorf("reopen port %s: %w", f.portStr, lastErr)
}

// ChipType returns the detected chip type.
func (f *Flasher) ChipType() ChipType {
if f.chip != nil {
Expand Down Expand Up @@ -233,6 +261,11 @@ func (f *Flasher) connect() error {
}
time.Sleep(50 * time.Millisecond)
}

// Sync failed — try reopening port (USB CDC may have re-enumerated)
if err := f.reopenPort(); err != nil {
continue // port reopen failed, try next attempt
}
}

return &SyncError{Attempts: attempts}
Expand All @@ -257,9 +290,21 @@ synced:

f.logf("Detected chip: %s", f.chip.Name)

// Run chip-specific post-connect initialization.
if f.chip.PostConnect != nil {
if err := f.chip.PostConnect(f); err != nil {
f.logf("Warning: post-connect: %v", err)
}
}

// Propagate chip capabilities to the connection layer.
f.conn.setSupportsEncryptedFlash(f.chip.SupportsEncryptedFlash)

// Propagate USB flag to connection layer for block size optimization.
if f.usesUSB {
f.conn.setUSB(true)
}

// Upload the stub loader to enable advanced features (erase, compression, etc.).
if s, ok := stubFor(f.chip.ChipType); ok {
f.logf("Loading stub loader...")
Expand Down Expand Up @@ -688,6 +733,20 @@ func (f *Flasher) WriteRegister(addr, value uint32) error {
return f.conn.writeReg(addr, value, 0xFFFFFFFF, 0)
}

// ReadFlash reads data from flash memory.
// Requires the stub loader to be running.
func (f *Flasher) ReadFlash(offset, size uint32) ([]byte, error) {
if !f.conn.isStub() {
return nil, &UnsupportedCommandError{Command: "read flash (requires stub)"}
}

if err := f.attachFlash(); err != nil {
return nil, err
}

return f.conn.readFlash(offset, size)
}

// Reset performs a hard reset of the device, causing it to run user code.
func (f *Flasher) Reset() {
if f.conn.isStub() {
Expand All @@ -702,7 +761,7 @@ func (f *Flasher) Reset() {
// CMD_FLASH_BEGIN after a compressed download may interfere with
// the flash controller state at offset 0. esptool also just does
// a hard reset without any flash commands for the ROM path.
hardReset(f.port, false)
hardReset(f.port, f.usesUSB)
f.logf("Device reset.")
}

Expand Down
15 changes: 15 additions & 0 deletions pkg/espflasher/flasher_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -433,3 +433,18 @@ func TestFlashSizeFromJEDECMatchesChipSizes(t *testing.T) {
}
}
}

func TestReadFlashRequiresStub(t *testing.T) {
mock := &mockConnection{}
mock.stubMode = false // ROM mode
f := &Flasher{conn: mock, chip: chipDefs[ChipESP32]}
_, err := f.ReadFlash(0, 1024)
if err == nil {
t.Fatal("expected error when stub is not running")
}
if ue, ok := err.(*UnsupportedCommandError); !ok {
t.Errorf("expected UnsupportedCommandError, got %T: %v", err, err)
} else if ue.Command != "read flash (requires stub)" {
t.Errorf("unexpected error message: %s", ue.Command)
}
}
85 changes: 79 additions & 6 deletions pkg/espflasher/protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ const (
// flashSectorSize is the minimum flash erase unit.
flashSectorSize uint32 = 0x1000 // 4KB

// readFlashBlockSize is the block size for read flash operations.
readFlashBlockSize uint32 = 0x1000 // 4KB

// espImageMagic is the first byte of a valid ESP firmware image.
espImageMagic byte = 0xE9

Expand All @@ -79,7 +82,8 @@ const (
type conn struct {
port serial.Port
reader *slipReader
stub bool
stub bool
usesUSB bool // set for USB-OTG and USB-JTAG/Serial connections
// supportsEncryptedFlash indicates the ROM supports the 5th parameter
// (encrypted flag) in flash_begin/flash_defl_begin commands.
// Set based on chip type after detection.
Expand All @@ -91,6 +95,11 @@ func (c *conn) isStub() bool {
return c.stub
}

// setUSB sets whether the connection uses USB-OTG or USB-JTAG endpoints.
func (c *conn) setUSB(v bool) {
c.usesUSB = v
}

// setSupportsEncryptedFlash sets whether the ROM supports encrypted flash commands.
func (c *conn) setSupportsEncryptedFlash(v bool) {
c.supportsEncryptedFlash = v
Expand Down Expand Up @@ -125,8 +134,20 @@ func (c *conn) sendCommand(opcode byte, data []byte, chk uint32) error {
copy(pkt[8:], data)

frame := slipEncode(pkt)
_, err := c.port.Write(frame)
return err
// USB CDC endpoints have limited buffer sizes. Writing large SLIP frames
// in one shot can overflow the endpoint buffer and cause data loss.
// Chunk writes to 64 bytes (standard USB Full Speed bulk endpoint size).
const maxChunk = 64
for off := 0; off < len(frame); off += maxChunk {
end := off + maxChunk
if end > len(frame) {
end = len(frame)
}
if _, err := c.port.Write(frame[off:end]); err != nil {
return err
}
}
return nil
}

// commandResponse represents a parsed response from the ESP device.
Expand Down Expand Up @@ -541,6 +562,51 @@ func (c *conn) eraseRegion(offset, size uint32) error {
return err
}

// readFlash reads data from flash memory (stub-only).
func (c *conn) readFlash(offset, size uint32) ([]byte, error) {
data := make([]byte, 16)
binary.LittleEndian.PutUint32(data[0:4], offset)
binary.LittleEndian.PutUint32(data[4:8], size)
binary.LittleEndian.PutUint32(data[8:12], readFlashBlockSize)
binary.LittleEndian.PutUint32(data[12:16], 64) // max_inflight (stub clamps to 1)

if _, err := c.checkCommand("read flash", cmdReadFlash, data, 0, defaultTimeout, 0); err != nil {
return nil, err
}

blockTimeout := defaultTimeout + time.Duration(readFlashBlockSize/256)*100*time.Millisecond
numBlocks := (size + readFlashBlockSize - 1) / readFlashBlockSize
result := make([]byte, 0, size)

for i := uint32(0); i < numBlocks; i++ {
// Read SLIP-framed data block
block, err := c.reader.ReadFrame(blockTimeout)
if err != nil {
return nil, fmt.Errorf("read flash block %d/%d: %w", i+1, numBlocks, err)
}
result = append(result, block...)

// Send ACK: cumulative bytes received (SLIP-framed)
ack := make([]byte, 4)
binary.LittleEndian.PutUint32(ack, uint32(len(result)))
ackFrame := slipEncode(ack)
if _, err := c.port.Write(ackFrame); err != nil {
return nil, fmt.Errorf("read flash ACK %d/%d: %w", i+1, numBlocks, err)
}
}

// Read final 16-byte MD5 digest (SLIP-framed)
_, err := c.reader.ReadFrame(defaultTimeout)
if err != nil {
return nil, fmt.Errorf("read flash MD5: %w", err)
}

if uint32(len(result)) > size {
result = result[:size]
}
return result, nil
}

// flashWriteSize returns the appropriate block size based on loader type.
func (c *conn) flashWriteSize() uint32 {
if c.stub {
Expand Down Expand Up @@ -601,17 +667,24 @@ func (c *conn) loadStub(s *stub) error {

// uploadToRAM writes a binary segment to the device's RAM via mem_begin/mem_data.
func (c *conn) uploadToRAM(data []byte, addr uint32) error {
// USB CDC endpoints have limited buffer sizes. Use 1KB blocks for
// USB connections instead of the default 6KB to avoid timeout.
blockSize := espRAMBlock
if c.usesUSB {
blockSize = 0x400 // 1KB
}

dataLen := uint32(len(data))
numBlocks := (dataLen + espRAMBlock - 1) / espRAMBlock
numBlocks := (dataLen + blockSize - 1) / blockSize

if err := c.memBegin(dataLen, numBlocks, espRAMBlock, addr); err != nil {
if err := c.memBegin(dataLen, numBlocks, blockSize, addr); err != nil {
return err
}

seq := uint32(0)
offset := uint32(0)
for offset < dataLen {
end := offset + espRAMBlock
end := offset + blockSize
if end > dataLen {
end = dataLen
}
Expand Down
Loading
Loading