Skip to content

Commit 4f32453

Browse files
bwhitmanclaude
andauthored
amyboard: report full sketch traceback (line numbers) to web editor (#1059)
When a sketch fails to load, run_sketch() reported only "<ExcType>: <message>" back to the web editor over the 'X' sysex frame — the full traceback (with file/line numbers) was generated by sys.print_exception() but only went to the serial console, never the website. In simulate mode the web already shows the full traceback (it scrapes it off the JS console), so this gap only affected control mode on real hardware. Capture sys.print_exception() into an io.StringIO and report that instead, via a new _format_exc_for_report() helper. Kept as a single sysex frame (no chunking): real MicroPython tracebacks are ~150-300 bytes and a single frame reliably carries ~768 raw bytes over Web MIDI (ZDUMP_STREAM_RAW_CHUNK in amy/src/transfer.c). The helper bounds output to 700 bytes, keeping the tail (MicroPython prints most-recent-call-last, so the exception line is last) and falling back to the old one-liner if capture ever fails. The web display already renders multi-line text, so no web-side change is needed. The sketch.loop() runtime-error path is intentionally left serial-only: it runs every ~60ms and reporting from there would spam sysex frames. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 1ad990b commit 4f32453

1 file changed

Lines changed: 27 additions & 2 deletions

File tree

tulip/shared/amyboard-py/amyboard.py

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,31 @@ def start_amy():
512512

513513
_sketch_seq = None # Keep reference to prevent GC
514514

515+
# Cap on the traceback text we ship in a single 'X' sysex frame. One sysex
516+
# frame is reliably delivered over Web MIDI up to ~768 raw bytes (see
517+
# ZDUMP_STREAM_RAW_CHUNK in amy/src/transfer.c); we stay comfortably under that
518+
# after base64 expansion. Real MicroPython sketch tracebacks are ~150-300 bytes,
519+
# so this only ever trims pathologically deep stacks.
520+
_SKETCH_ERROR_MAX = 700
521+
522+
def _format_exc_for_report(e):
523+
"""Full traceback (with file/line numbers) as a string, bounded so it fits
524+
in a single error sysex frame. MicroPython prints most-recent-call-last, so
525+
the exception type/message is the LAST line — when we must truncate we keep
526+
the tail (the part the user needs) and drop the outermost frames."""
527+
try:
528+
from io import StringIO
529+
buf = StringIO()
530+
sys.print_exception(e, buf)
531+
text = buf.getvalue().strip()
532+
except Exception:
533+
# If capture fails for any reason, fall back to the old one-liner so the
534+
# web still gets *something* rather than nothing.
535+
text = "%s: %s" % (type(e).__name__, e)
536+
if len(text) > _SKETCH_ERROR_MAX:
537+
text = "...(traceback truncated)\n" + text[-_SKETCH_ERROR_MAX:]
538+
return text
539+
515540
def _report_sketch_error(detail):
516541
"""Push a sketch load error back to the web editor over sysex, framed as
517542
F0 00 03 45 'X' <base64 text> F7, so the UI can surface the failure (banner +
@@ -570,7 +595,7 @@ def run_sketch():
570595
# Route through stderr_write so the web console sees it alongside the
571596
# other diagnostics (stdout may not be wired up in some hosts).
572597
tulip.stderr_write("sketch.py load failed: %s: %s" % (type(e).__name__, e))
573-
_report_sketch_error("%s: %s" % (type(e).__name__, e))
598+
_report_sketch_error(_format_exc_for_report(e))
574599
try:
575600
sys.print_exception(e)
576601
except Exception:
@@ -588,7 +613,7 @@ def run_sketch():
588613
import sketch
589614
except Exception as e2:
590615
tulip.stderr_write("default sketch.py load also failed: %s: %s" % (type(e2).__name__, e2))
591-
_report_sketch_error("%s: %s" % (type(e2).__name__, e2))
616+
_report_sketch_error(_format_exc_for_report(e2))
592617
try:
593618
sys.print_exception(e2)
594619
except Exception:

0 commit comments

Comments
 (0)