Skip to content

Browser interface: the Firmware button flashes an ESP board with esptool - #92

Merged
bdbarnett merged 6 commits into
mainfrom
browser-firmware
Oct 8, 2026
Merged

bdbarnett merged 6 commits into
mainfrom
browser-firmware

Conversation

@bdbarnett

Copy link
Copy Markdown
Collaborator

The browser interface's Firmware button now flashes an ESP board. It opens a small dialog: choose a firmware .bin from your computer, check the serial port (the connected board's by default), tick "Erase all of flash first" if you want a clean board, and press Flash. The server lets go of the board, runs the CLI's own esptool path (python -m mpftp.firmware flash, the engine mpftp firmware flash and the extension's Firmware panel already use) at the offset the image's chip needs, streams esptool's output and a progress bar into the dialog, and the page reconnects once the board restarts.

It is esptool only. A .uf2 (by name or by its magic) and a connected board whose sys.platform isn't an ESP are refused with "For UF2 boards, drag the .uf2 onto the board's drive." The extension's Firmware panel is unchanged.

How it works

  • ui/src/firmware.ts: the dialog. The file goes to the server in 512 KB chunks (firmware_upload), then firmware_flash runs while firmware_log / firmware_progress notifications fill the dialog.
  • cli/src/mpftp/webflash.py: the server side, answered by the relay rather than the sidecar. It checks the image before touching the board: ESP image magic, the chip from the image header, the offset for that chip, and that it isn't an app image aimed at the bootloader offset. Then it releases the board. A board on a USB-UART bridge or the chip's USB-Serial/JTAG keeps its port. A board whose REPL is MicroPython's TinyUSB CDC (S2/S3 on native USB) is sent to its bootloader and the ROM download port that appears is flashed instead, as the extension does. If no download port appears, the dialog says to hold BOOT and tap RESET.

Thonny's esptool dialog compared with ours

I read Thonny 5.0.0's plugins/micropython/esptool_dialog.py (with base_flashing_dialog.py, workdlg.py and plugins/esp/__init__.py), which bundles esptool 5.2.0.

Thonny mpftp (CLI engine, now also the browser)
Port User picks from all serial ports The connected board's port by default; any port can be typed
Releasing the REPL proxy.disconnect() if Thonny holds that port, sleep 1.5 s, then open and close the port to prove it is free (another 1.5 s), with a "Can't connect" error if not Sidecar repl_stop + disconnect, wait for the port to be listed again, settle 1 s. A busy port's esptool error becomes "another program has it ... close it and Flash again"
Native USB (TinyUSB CDC) Nothing special: the instructions tell the user to hold BOOT machine.bootloader(), wait up to 15 s for the ROM's download port (303A:1001 or a new 303A port), flash that
Chip image_info on the file gives the family, passed as --chip Before: no --chip (esptool auto-detects). Now: --chip from the image header, so a board that is another chip is refused before anything is written
Offset 0x1000 for esp32/esp32s2, else 0x0; overridable From board.json when building, else the chip: 0x1000 (ESP32, S2), 0x2000 (P4, and C5, which was wrong at 0x0 before), 0x0 (S3, C2, C3, C6, C61, H2). Checked against ESP-IDF 5.5.4's CONFIG_BOOTLOADER_OFFSET_IN_FLASH and esptool's BOOTLOADER_FLASH_OFFSET. Thonny has no 0x2000 case, so a P4 or C5 image goes to 0x0 there
Erase Checkbox, on by default, as write_flash --erase-all in the same run Before: a separate erase-flash run, then write-flash. Now: one write-flash --erase-all run, so the board is reset into its ROM loader once, not twice (the second reset is the one a native-USB board can miss)
Partition table Not checked Read from the board first; a changed table refuses the flash until Erase is ticked (the dialog ticks it and says why)
Baud 115200 default; 460800/230400/38400/9600 offered by hand 460800. Now: a UART link that drops mid-write retries once at 115200 (not for native USB, a wrong chip, a busy port or a partition-table refusal)
Flash mode / size / freq Exposed, default keep Not passed: esptool's default is keep
--no-stub Offered Not offered
--before / --after esptool defaults (default_reset / hard_reset) default-reset / hard-reset, overridable on the CLI
Progress Each output line (universal newlines split esptool's \r) shown as the action text; indeterminate bar esptool's Writing at ... NN.N% lines drive a real percentage; other lines go to the log (blank padding dropped)
Afterwards Nothing: the user restarts the backend Hard reset by esptool, then the page reconnects (up to 30 s) and the REPL comes back

Adopted from Thonny: --chip from the image, erase-and-write in one run, a plain reason when another program holds the port, and a slower-baud fallback (automatic here, manual there). Not adopted: the flash-mode/size/--no-stub knobs, which the maintainer asked to keep out of a minimal dialog.

The ⋯ menu, toolbar and context menus, exercised in the browser

Driven with Playwright (Chromium) against the LCD-7 (ESP32-S3, MicroPython 1.29.0) on COM17. VS Code behaviour is from extension.ts and FtpViewProvider.ts.

Entry VS Code Browser Verdict
⋯ Connect to Board Port quick pick Device dialog, connected to COM17 kept
⋯ Disconnect Closes the session Footer "Disconnected" kept
⋯ Resume Last Device Reconnects the last port "mpftp resumed: COM17", connected kept
⋯ Refresh Serial Ports Refreshes the status-bar port list (the Connect dialog has its own Refresh) hidden (already, #85)
⋯ Interrupt (Ctrl+C) Sends Ctrl+C "Interrupt (Ctrl+C) sent" kept
⋯ Soft Reset Soft reset "Soft reset sent (main.py not run)" kept
⋯ Hard Reset Reset + reconnect "mpftp reconnected: COM17" kept
⋯ Open REPL Opens the REPL terminal Focuses the page's REPL kept
⋯ Run Editor Buffer Runs the active editor tab Runs the page editor's tab: buffer ran 42 in the REPL kept
⋯ Open File Transfer in Panel / Editor Moves the panel No meaning on a page hidden (already)
⋯ Build & Flash Firmware… Opens the Firmware panel Said "not in the browser yet" fixed: flashes; named "Flash Firmware (esptool)…" in the browser
⋯ Enable / Disable Wi-Fi Access boot.py change with a diff Same dialog; "Show the change to disable" reports no mpftp block (nothing written) kept
⋯ Edit Board File Opens a board file in an editor Opens it in the page editor kept
⋯ Eval Expression Info message Toast 3 for 1 + 2 kept
⋯ Exec Code Output channel REPL shows exec says 42 kept
⋯ Enter Bootloader Bootloader, disconnect Same; board left in ROM mode kept
⋯ Get RTC / Set RTC from Host Info message Toasts with the date kept
⋯ Install Package mip/circup, output channel Installed functools to /lib; the status line stayed on "Installing…" fixed: status line updates (and on failure)
⋯ Disk Free (df) Output channel Mount table in the REPL kept
⋯ Mount Local Folder / Unmount mpremote mount of a picked folder Needs a folder on the sidecar's host hidden (already)
⋯ ROMFS Query Output channel "ROMFS is not enabled on this device" in the REPL kept
⋯ Hash Board File Info message Toast with the SHA-256 kept
⋯ Agent Status VS Code output No meaning on a page hidden (already)
Toolbar Connect, Interrupt, Soft Reset, Hard Reset, REPL As the menu As the menu, all worked kept
Toolbar Firmware Firmware panel Flash dialog, port prefilled fixed
Local: New folder, New file, Rename, Delete, Refresh, Browse, Open in Editor, Upload & Run (buttons and right-click) Local file ops All worked; Browse asks for a path (a page can't hand over a folder) kept
Local right-click Upload / Open (folder) / .. Upload, cd Worked kept
Board: New folder, New file, Rename, Delete, Refresh, Run, Open in Editor (buttons and right-click) Board file ops All worked, incl. recursive folder delete kept
Board right-click Run board file, SHA-256, Download, Open (folder) As named All worked kept
Transfer arrows Upload / download selection Worked kept
Editor Ctrl+S on a board file Writes it back Edited file ran with the new line kept

So nothing new needed hiding: the entries that only make sense in VS Code were already hidden by #85's host command list. ftp.js now also accepts a host-supplied commandTitles map so the browser can call the entry what it does there; VS Code sends none and is unchanged.

Also fixed on the way

  • Large WebSocket messages. Chromium sends a message over about 128 KB as a first frame plus continuation frames; the server read each frame as a whole message, so a firmware chunk (and a large editor save) never arrived and its fragments went to the sidecar as garbage. Frames are now joined until the final one, and unmasking XORs the whole payload at once instead of a byte at a time. The new codec test fails on main and passes here.

Proof

  • Real board, through the page in Playwright: the LCD-7's flash (bootloader, partition table and app, 0x0 to 0x460000) was read back with esptool first, then written back through the Firmware dialog twice. Both runs: "partition layout matches", --chip esp32s3, 4,587,520 bytes at 0x0 in 61.5 s, "Hash of data verified", hard reset, "Reconnected to COM17" at about 80 s, and Exec Code on the reconnected board printed after flash: v1.29.0-1.g0484529dd1.dirty on 2026-10-07 esp32. The board's files were untouched.
  • Not tried on hardware: Erase (it would wipe the board's files), the native-USB bootloader path (S2/S3 on TinyUSB CDC) and the 115200 retry. Those are covered by unit tests with a mocked sidecar, and the real engine runs against cli/tests/fixtures/fake-esptool.
  • Scoped tests: test_webflash (new, 19), test_pwa, test_app_image_offset, test_panel, test_firmware_download, test_partition_table, test_detect.

Choose a .bin, check the port and Erase, press Flash: the server lets go
of the board (through the ROM download port for a TinyUSB CDC board),
runs the CLI's own esptool path at the chip's offset, streams progress
and reconnects. UF2 files and non-ESP boards are refused with a hint.

Also: esptool gets --chip from the image header, --erase is one
write-flash --erase-all run, the C5's offset is 0x2000, the page's
WebSocket joins continuation frames (Chromium splits messages over
~128 KB), the menu calls the entry Flash Firmware in the browser, and
the status line no longer sticks at Installing after a package install.
# Conflicts:
#	CHANGELOG.md
#	cli/src/mpftp/webui/app.css
#	cli/src/mpftp/webui/app.js
#	ui/src/main.ts
@bdbarnett
bdbarnett merged commit 6f2694b into main Oct 8, 2026
4 checks passed
@bdbarnett
bdbarnett deleted the browser-firmware branch October 8, 2026 06:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant