@@ -70,6 +70,25 @@ def flash(port, bin_path, baud):
7070 subprocess .run (cmd , check = True )
7171
7272
73+ def reset_via_chipid (port ):
74+ """Clean-boot the board with an esptool `chip-id`: it enters download mode,
75+ reads the chip id (returning the MAC), then hard-resets and releases the
76+ port. On this bench the flash's own `--after hard-reset` can leave the board
77+ wedged in a boot-loop, but a chip-id cycle reliably boots it. Returns True
78+ only if the MAC came back -- i.e. esptool actually completed the handshake
79+ and reset the board (a bare `chip-id` defaults to --before default-reset /
80+ --after hard-reset). Give the board a few seconds afterward to boot and
81+ enumerate USB-MIDI before looking for it (see --boot-settle)."""
82+ cmd = [sys .executable , "-m" , "esptool" , "--chip" , "esp32s3" , "--port" , port , "chip-id" ]
83+ print ("[reset]" , " " .join (cmd ))
84+ r = subprocess .run (cmd , capture_output = True , text = True )
85+ sys .stdout .write (r .stdout )
86+ ok = r .returncode == 0 and "MAC:" in r .stdout
87+ if not ok :
88+ print ("[reset] chip-id did not return a MAC -- board/dongle not responding" )
89+ return ok
90+
91+
7392# ── MIDI / sysex backends ───────────────────────────────────────────────────
7493def alsa_find_midi ():
7594 out = subprocess .run (["amidi" , "-l" ], capture_output = True , text = True ).stdout
@@ -357,9 +376,12 @@ def main():
357376 ap .add_argument ("--port" , help = "esptool serial port (the USB-serial dongle)" )
358377 ap .add_argument ("--baud" , type = int , default = 921600 )
359378 ap .add_argument ("--boot-attempts" , type = int , default = 3 ,
360- help = "flash+ boot tries before failing. Retries the bench's "
361- "intermittent early- boot flake (a real boot failure fails "
379+ help = "chip-id reset + boot tries before failing. Retries the "
380+ "bench's intermittent boot-wedge (a real boot failure fails "
362381 "every try)." )
382+ ap .add_argument ("--boot-settle" , type = float , default = 5.0 ,
383+ help = "seconds to wait after a chip-id reset for the board to "
384+ "boot and enumerate USB-MIDI before looking for it." )
363385 ap .add_argument ("--no-flash" , action = "store_true" )
364386 ap .add_argument ("--flash-only" , action = "store_true" )
365387 ap .add_argument ("--midi-port" , help = "ALSA hw:X,Y or mido name (auto if omitted)" )
@@ -395,53 +417,50 @@ def main():
395417 sys .exit ("--port (the USB-serial dongle) is required to flash." )
396418 bin_path , tmp = resolve_firmware (args ) if not args .no_flash else (None , False )
397419
398- # The debug UART (the dongle on the board's debug header) is a stable hardware
399- # UART carrying the ESP-IDF console; opening it pulses DTR/RTS through the
400- # board's auto-reset circuit, so the board reboots as we connect — which is
401- # exactly how we capture a clean boot log. The native CDC, by contrast,
402- # re-enumerates during boot (ROM USB-Serial-JTAG -> TinyUSB CDC), so we start
403- # tailing it only *after* the board is back up, for clean MicroPython stdout.
420+ # We deliberately do NOT hold the debug UART open while the board boots: the
421+ # proven-good reset on this bench is `esptool chip-id` (hard-reset + release
422+ # the port), wait a few seconds, then `amidi -l` -- holding the dongle's
423+ # DTR/RTS lines across boot is what left the board wedged in a boot-loop. The
424+ # consoles are tailed only *after* MIDI is up.
404425 debug_port = args .debug_port or args .port or ("/dev/ttyACM1" if ALSA else None )
405426 loggers = []
406427 try :
407- # Flash + boot, retrying the bench's intermittent early-boot flake. The
408- # firmware is fine — its 2nd-stage bootloader is byte-identical to builds
409- # that boot — but a transient flash-read/power/reset glitch after the long
410- # USB flash can panic-loop the board before USB-MIDI enumerates. Re-flashing
411- # forces ROM download mode (a known-good reset) and usually clears it; a
412- # genuinely broken build instead fails on every attempt.
428+ # Flash once, then bring the board up with a clean esptool chip-id reset
429+ # cycle. A plain flash hard-reset can leave it wedged in a boot-loop on
430+ # this bench; a chip-id cycle reliably boots it. Retries do the (cheap)
431+ # reset again, not a re-flash -- the firmware is already written + verified.
432+ if not args .no_flash :
433+ flash (args .port , bin_path , args .baud )
434+ if args .flash_only :
435+ print ("[done] flashed." ); return 0
413436 for attempt in range (1 , args .boot_attempts + 1 ):
414- if not args .no_flash :
415- flash (args .port , bin_path , args .baud )
416- if args .flash_only :
417- print ("[done] flashed." ); return 0
418- dbg = None
419- if not args .no_serial_log :
420- label = "DEBUG UART (ESP-IDF console / stderr)"
421- if args .boot_attempts > 1 :
422- label += f" [boot { attempt } /{ args .boot_attempts } ]"
423- dbg = start_serial_log (debug_port , args .debug_baud , label )
424- if dbg :
425- loggers .append (dbg )
426- time .sleep (2.0 ) # let the connect-reset drop the old USB enumeration
437+ if args .port :
438+ reset_via_chipid (args .port )
439+ time .sleep (args .boot_settle ) # board boots + USB-MIDI enumerates
427440 args .midi_port = args .midi_port or wait_for_board ()
428441 if args .midi_port :
429442 break
430- # Boot failed: stop this attempt's logger so esptool can drive the
431- # dongle's reset lines, then retry. The crash log stays in `loggers`.
432- if dbg :
433- dbg .stop ()
434- time .sleep (0.5 ) # let the OS release the port before re-flashing
435443 if attempt < args .boot_attempts :
436- print (f"[boot] attempt { attempt } /{ args .boot_attempts } failed ; "
437- "re-flashing and retrying" )
444+ print (f"[boot] attempt { attempt } /{ args .boot_attempts } : MIDI not up ; "
445+ "re-resetting (chip-id) and retrying" )
438446 else :
447+ # Real boot failure -- grab a short debug-UART snapshot so it's still
448+ # debuggable (the only time we open the dongle before MIDI is up).
449+ if not args .no_serial_log :
450+ snap = start_serial_log (debug_port , args .debug_baud ,
451+ "DEBUG UART (post-failure snapshot)" )
452+ if snap :
453+ loggers .append (snap )
454+ time .sleep (3.0 )
439455 sys .exit (f"[boot] board never enumerated MIDI after { args .boot_attempts } "
440- "attempt(s) — bench boot flake that didn't clear, or a real "
441- "boot failure (see serial log)." )
456+ "chip-id reset attempt(s) — see serial log." )
442457
443458 time .sleep (1.5 ) # let the MIDI endpoint + synth settle post-boot
444459 if not args .no_serial_log :
460+ # CDC is the board's own USB (no reset lines), so opening it mid-run
461+ # can't perturb the board or the recording. We skip the debug UART
462+ # here for the same reason -- its log only matters on boot failure
463+ # (captured in the snapshot above).
445464 cdc = start_serial_log (args .cdc_port , 115200 , "CDC (MicroPython stdout)" )
446465 if cdc :
447466 loggers .append (cdc )
0 commit comments