Context
I drove a Waveshare ESP32-P4 (720×720 DSI, C6 Wi-Fi) entirely through scripts/mpftp from a Claude Code session: detect → build firmware with user C modules → flash → provision Wi-Fi → mip install → run examples → instrument LVGL → diagnose two real bugs → rebuild → reflash → verify. It worked, and the firmware half in particular was excellent. Everything below is friction I hit doing that, ordered by how much time it cost.
1. A read that doesn't interrupt the running program (biggest one)
exec / get / put all enter the raw REPL, which Ctrl-Cs whatever is running and clears module state. Observing a running script is what kills it. Probe after probe appeared to "stall at stage 00" — every one of those stalls was my own get interrupting the script mid-import. I lost more time to this than to the actual bugs.
The workaround I converged on:
- script appends progress to
/result.txt, one line per stage
mpftp run -d COM4 script.py (--no-follow, returns at once)
- wait without touching the port
get /result.txt
Requests:
mpftp watch-file /result.txt or a --no-interrupt read path that streams a board file without entering raw REPL.
- Make the second port a first-class read channel. This board exposes
role=repl (UART) + role=cdc_debug (native CDC); debug-tee is the right idea but see §5.
- Failing both, document the write-to-file pattern in
docs/agent-guide.md — it's the single most useful technique I found and I had to derive it.
2. One command for the run → wait → collect cycle
I typed this shape maybe thirty times:
mpftp hard-reset -d COM4; sleep 10
mpftp run -d COM4 probe.py; sleep 20
mpftp get -d COM4 /result.txt ./result.txt; cat ./result.txt
Proposal:
mpftp probe -d COM4 probe.py --reboot-first --capture /result.txt --wait 20
…emitting the captured file as JSON. This is the agent loop for anything that outlives a raw-REPL session.
3. --reboot-first, because stale state looks like breakage
Re-importing board_config re-initialises the display and leaves prior timers armed. Two or three iterations in, the board saturates and every symptom looks like a code bug. Brad had to tell me "you likely need to reboot between iterations" — after which a probe that had been failing passed immediately. A --reboot-first flag on run/exec (and a note in the guide) would save agents that whole detour.
Related: hard-reset runs main.py. When main.py is a previous app, every reset boots into it. A --skip-main / --safe-boot option, or a documented "rename main.py aside while iterating", would help.
4. WSLENV for micropython.exe (silent wrong-code bug)
Launching the Windows binary from WSL, it never receives MICROPYPATH unless WSLENV=MICROPYPATH/l is exported, and silently falls back to %USERPROFILE%\.micropython\lib. I spent a while convinced I'd broken appdev — the board was running a months-old installed copy. mpftp knows it's launching a Windows binary; it should set WSLENV itself, or warn when MICROPYPATH is set but unreachable by the target interpreter.
5. debug-tee didn't work from WSL
mpftp debug-tee COM42 --log-path /tmp/tee.log reported "log_path": "\\tmp\\tee.log" and no file appeared at /tmp/tee.log, /mnt/c/tmp/tee.log, or ~/.mpftp/. WSL path translation on the sidecar side. This is the feature that would have solved §1, so it's worth fixing first.
6. firmware build --clean doesn't clear a foreign CMake cache
The build dir held a cache pointing at /home/brad/gh/other/esp-idf (an SDK that had since moved). --clean ran idf.py fullclean, which refused ("doesn't seem to be a CMake build directory") and left the bootloader subproject cache intact; the next build failed on does not match the source ... used to generate cache. rm -rf build-<BOARD>-<VARIANT> fixed it. Suggest --clean fall back to removing the build dir when fullclean refuses, or a --distclean.
7. Machine-readable errors
Several failures came back as bare text where success is JSON — Expecting value: line 1 column 1 (char 0) from a wedged port, [Errno 2] /result.txt from a missing file. Agents parse JSON; a consistent {"ok": false, "error": ..., "hint": ...} envelope plus meaningful exit codes would let a tool retry intelligently instead of guessing.
Credit where due: "timeout waiting for first EOF reception" already does this well — it explains that the board is fine and busy, names --no-follow, and says the COM handle was released. More errors like that one.
8. Smaller things
put --verify returning a sha256 is genuinely reassuring — keep it, and consider --verify by default.
firmware detect reporting secure-boot / flash-encryption before an erase is exactly the right safety gate.
- Board debris accumulates fast when probing (
/probe_*.py, /result.txt). A mpftp clean --dry-run would be nice.
mip install defaulted to micropython.org/pi/v2 and reported Package not found for a package that exists on a different index; passing index= fixed it. Worth surfacing in the CLI as mpftp mip --index.
9. Claude Code integration
Brad raised this and I think it's right. The CLI is already agent-shaped — JSON out, one concern per subcommand, no interactive prompts. Two paths:
- MCP server: expose
ports / connect / run / probe / firmware * as tools. An agent gets typed schemas instead of parsing --help, and the non-interrupting read from §1 becomes a natural tool boundary.
- Claude Code plugin: skills wrapping the workflows (
flash this board, run this example, bisect this firmware) plus the agent guide as skill context. The write-to-file probe pattern is exactly the kind of hard-won technique a skill should encode so nobody re-derives it.
The docs are already unusually good for this — docs/agent-guide.md is written to agents, and the role=repl vs role=cdc_debug table answered my port question without asking. That foundation is why the session worked at all.
🤖 Generated with Claude Code
Context
I drove a Waveshare ESP32-P4 (720×720 DSI, C6 Wi-Fi) entirely through
scripts/mpftpfrom a Claude Code session: detect → build firmware with user C modules → flash → provision Wi-Fi →mip install→ run examples → instrument LVGL → diagnose two real bugs → rebuild → reflash → verify. It worked, and the firmware half in particular was excellent. Everything below is friction I hit doing that, ordered by how much time it cost.1. A read that doesn't interrupt the running program (biggest one)
exec/get/putall enter the raw REPL, which Ctrl-Cs whatever is running and clears module state. Observing a running script is what kills it. Probe after probe appeared to "stall at stage 00" — every one of those stalls was my owngetinterrupting the script mid-import. I lost more time to this than to the actual bugs.The workaround I converged on:
/result.txt, one line per stagempftp run -d COM4 script.py(--no-follow, returns at once)get /result.txtRequests:
mpftp watch-file /result.txtor a--no-interruptread path that streams a board file without entering raw REPL.role=repl(UART) +role=cdc_debug(native CDC);debug-teeis the right idea but see §5.docs/agent-guide.md— it's the single most useful technique I found and I had to derive it.2. One command for the run → wait → collect cycle
I typed this shape maybe thirty times:
Proposal:
…emitting the captured file as JSON. This is the agent loop for anything that outlives a raw-REPL session.
3.
--reboot-first, because stale state looks like breakageRe-importing
board_configre-initialises the display and leaves prior timers armed. Two or three iterations in, the board saturates and every symptom looks like a code bug. Brad had to tell me "you likely need to reboot between iterations" — after which a probe that had been failing passed immediately. A--reboot-firstflag onrun/exec(and a note in the guide) would save agents that whole detour.Related:
hard-resetrunsmain.py. Whenmain.pyis a previous app, every reset boots into it. A--skip-main/--safe-bootoption, or a documented "rename main.py aside while iterating", would help.4. WSLENV for
micropython.exe(silent wrong-code bug)Launching the Windows binary from WSL, it never receives
MICROPYPATHunlessWSLENV=MICROPYPATH/lis exported, and silently falls back to%USERPROFILE%\.micropython\lib. I spent a while convinced I'd brokenappdev— the board was running a months-old installed copy. mpftp knows it's launching a Windows binary; it should setWSLENVitself, or warn whenMICROPYPATHis set but unreachable by the target interpreter.5.
debug-teedidn't work from WSLmpftp debug-tee COM42 --log-path /tmp/tee.logreported"log_path": "\\tmp\\tee.log"and no file appeared at/tmp/tee.log,/mnt/c/tmp/tee.log, or~/.mpftp/. WSL path translation on the sidecar side. This is the feature that would have solved §1, so it's worth fixing first.6.
firmware build --cleandoesn't clear a foreign CMake cacheThe build dir held a cache pointing at
/home/brad/gh/other/esp-idf(an SDK that had since moved).--cleanranidf.py fullclean, which refused ("doesn't seem to be a CMake build directory") and left the bootloader subproject cache intact; the next build failed ondoes not match the source ... used to generate cache.rm -rf build-<BOARD>-<VARIANT>fixed it. Suggest--cleanfall back to removing the build dir when fullclean refuses, or a--distclean.7. Machine-readable errors
Several failures came back as bare text where success is JSON —
Expecting value: line 1 column 1 (char 0)from a wedged port,[Errno 2] /result.txtfrom a missing file. Agents parse JSON; a consistent{"ok": false, "error": ..., "hint": ...}envelope plus meaningful exit codes would let a tool retry intelligently instead of guessing.Credit where due:
"timeout waiting for first EOF reception"already does this well — it explains that the board is fine and busy, names--no-follow, and says the COM handle was released. More errors like that one.8. Smaller things
put --verifyreturning a sha256 is genuinely reassuring — keep it, and consider--verifyby default.firmware detectreporting secure-boot / flash-encryption before an erase is exactly the right safety gate./probe_*.py,/result.txt). Ampftp clean --dry-runwould be nice.mip installdefaulted tomicropython.org/pi/v2and reportedPackage not foundfor a package that exists on a different index; passingindex=fixed it. Worth surfacing in the CLI asmpftp mip --index.9. Claude Code integration
Brad raised this and I think it's right. The CLI is already agent-shaped — JSON out, one concern per subcommand, no interactive prompts. Two paths:
ports/connect/run/probe/firmware *as tools. An agent gets typed schemas instead of parsing--help, and the non-interrupting read from §1 becomes a natural tool boundary.flash this board,run this example,bisect this firmware) plus the agent guide as skill context. The write-to-file probe pattern is exactly the kind of hard-won technique a skill should encode so nobody re-derives it.The docs are already unusually good for this —
docs/agent-guide.mdis written to agents, and therole=replvsrole=cdc_debugtable answered my port question without asking. That foundation is why the session worked at all.🤖 Generated with Claude Code