The proWebFlash seam gains an optional flashMicroPython(files) method
plus an 'uploading' progress phase. MicroPython boards enable the
Flash menu when the overlay implements it — no compile step needed,
the modal collects the workspace group's files (same path as the Run
flow) and explains the two-step install+upload. toFirmwareVariant is
exported so the flasher shares the emulator's per-chip offset table.
compiledProgram holds the 'micropython-loaded' sentinel in MicroPython
mode, not a flash image — atob() blew up with an encoding DOMException
before the port chooser even opened. The menu item now disables with an
explanatory tooltip and the modal shows the limitation instead of the
Connect button.
The board context menu's Flash item and FlashModal gain a second
backend: a Web Serial flasher the pro overlay can install via
lib/proWebFlash.ts (installWebFlashImpl). Without an overlay the seam
is inert — web builds keep the desktop-only behavior. esptool-js is
declared here because the overlay has no package.json of its own; OSS
code never imports it (verified absent from the vite bundle).
Brings hardware flashing into Velxio Desktop. Per-board "Flash to
real board" entry in the canvas context menu opens a modal that
enumerates USB serial ports, lets the user pick one, then
streams arduino-cli upload output live until the board is flashed.
Backend (Phase D1) — backend/app/api/routes/flash.py (new):
POST /api/flash/upload (multipart: board_id, port, fqbn,
program_format, program)
→ SSE stream of {phase, line?, progress?} events
→ final {phase:'done', success, elapsed_ms, error?}
- Wraps `arduino-cli upload -p <port> -i <file> --fqbn <fqbn> -v`
so AVR (avrdude), ESP32 (esptool), RP2040 (picotool), SAMD
(bossac) all share one code path — arduino-cli internally
dispatches by FQBN.
- Per-port asyncio.Lock prevents two simultaneous flashes from
fighting over the same /dev/ttyACM0.
- Allow-list of FQBN prefixes (arduino:avr, ATTinyCore:avr,
rp2040:rp2040, esp32:esp32, arduino:samd) so a typo can't
cause a confusing arduino-cli error.
- Format allow-list (hex / bin / uf2 / elf) drives the temp
file extension - arduino-cli uses the extension to route to
the right uploader.
- 8MB hard cap on the uploaded program (real sketches are
well under that; protects against a runaway frontend).
- X-Accel-Buffering: no header so nginx doesn't hold the SSE
chunks until the flash completes.
Frontend (Phase D3):
- frontend/src/services/flashService.ts (new):
async generator streamFlash() yields parsed SSE events.
Handles the base64-vs-text gotcha (compile returns hex_content
as text but binary_content as base64; for binary formats we
atob() into a Uint8Array before posting so the form upload
sends actual bytes, not the base64 ASCII).
- frontend/src/components/simulator/FlashModal.tsx (new):
Three-state UI: picking (port dropdown), flashing (progress
bar + live log), success/error (verdict + retry).
Empty-ports state shows a Linux dialout-group hint.
- SimulatorCanvas.tsx: board context menu gains "Flash to real
board" entry, gated on isTauri() + presence of compiledProgram.
Hidden in web (WebSerial is a separate sprint).
- tauriBridge.ts: SerialPortInfo type + listSerialPorts() helper
that invokes the Rust shell command added in Phase D2.
The sidecar already has arduino-cli on PATH (per
`pro/desktop/sidecar/main.py::_expose_bundled_arduino_cli`), so
no installer changes are needed — flash works the moment the
0.4.x desktop bundle ships with these commits.
Plan + remaining phase tracked in project/hardware-flashing/.
D2 (Rust serial enum) committed separately as a Tauri-shell-only
concern; D4 (manual smoke matrix with real boards) requires
physical hardware so it stays a follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>