User Guide¶
chumicro-repl ships four surfaces: an interactive line-mode editor (default), a byte-passthrough TUI, a one-shot tail() follow-mode, and a programmatic ReplSession.
All four share a pyserial wrapper, a UTF-8-safe streaming decoder, and a pattern detector for the kinds of output that matter (tracebacks, safe-mode banners, hard faults, soft reboots).
Install¶
Host-only. No bundle registration or device-side install needed. After install, a chumicro-repl console script is on your PATH.
Command-line interface¶
# Interactive TUI by serial path.
chumicro-repl --address /dev/cu.usbmodem14101
# One-shot tail for 5 seconds, fail on traceback (default).
chumicro-repl --address /dev/cu.usbmodem14101 --tail 5
# Tail without failing on traceback (useful for diagnosing crash loops).
chumicro-repl --address /dev/cu.usbmodem14101 --tail 30 --no-fail-on-traceback
By default in a TTY, chumicro-repl opens line mode — a host-side line editor that buffers each line locally before shipping to the device. Switch to byte-passthrough with --mode passthrough if you need raw-REPL framing or paste-mode forwarding.
Line mode¶
The default for terminal sessions. prompt_toolkit reads a complete line on the host — cursor edit, arrow keys, history navigation, Ctrl-R reverse search — then ships it to the device when you press Enter. The device responds in the friendly REPL; the response streams back through the same pattern detector + traceback highlighter tail() uses.
Lines starting with : are interpreted locally as builtin commands rather than shipped to the device:
| Command | What it does |
|---|---|
:help |
List every registered command. |
:edit |
Open $EDITOR with the last 10 input lines pre-seeded. On save+exit, every non-empty line ships line-by-line to the device. Falls back to vi when $EDITOR is unset. Honors shell-shaped values: EDITOR="code -w" works. |
:save NAME |
Persist the last 10 input lines to ~/.chumicro-repl/snippets/NAME.py. |
:load NAME |
Replay a saved snippet line-by-line. |
:snippets |
List saved snippet names. |
:rescan |
Drop the cached dir() so the next Tab re-queries the device. Use after import-ing a new module. |
:quit |
Exit without rebooting the device. EOF (Ctrl-D) / Ctrl-C at an empty prompt do the same. |
chumicro-repl · /dev/cu.usbmodem14101 · circuitpython · 115200 baud
:help to list commands · :quit to exit
>>> import wifi
>>> :rescan
line-mode: completion cache cleared; next Tab will re-query the device
>>> wifi.r<Tab>
radio
>>> wifi.radio.connect("MySSID", "secret")
>>> :save bringup-wifi
line-mode: saved 3 line(s) to ~/.chumicro-repl/snippets/bringup-wifi.py
>>> :quit
line-mode: bye
History is persistent per-device at ~/.chumicro-repl/history/<sanitized-address>/history.txt so a session on back-porch doesn't pollute one on greenhouse.
Tab completion¶
Two sources merge in line mode:
- Static catalog — Python keywords (
for,import,True, …) and public builtins (print,range,len, …). Always available; no device round-trip. - On-device
dir()— populated lazily on first Tab via aprint(repr(dir()))round-trip through raw REPL. Measured RTT on the supported CircuitPython and MicroPython boards is 8–45 ms — well below the perceptual "instant" threshold. Cached for the session;:rescaninvalidates after a newimport.
The friendly-banner reprint that the round-trip's Ctrl-B triggers is captured before line-mode resumes drawing, so it never leaks into your terminal. Embedding the round-trip in your own shape? Call chumicro_repl.completion.fetch_device_names(port).
CircuitPython's bare REPL has an empty user namespace by design — dir() returns only __name__ / __file__ until you import something. MicroPython pre-imports a handful of platform modules (gc, os, machine, rp2, vfs). The static catalog covers what's typed most regardless.
Passthrough mode (--mode passthrough)¶
When you need byte-exact forwarding — paste mode, raw-REPL framing, mpremote-shape Ctrl-C/Ctrl-D semantics — pass --mode passthrough. Keystrokes go straight to the device; the board does its own line editing.
Startup prints a dim banner identifying the connection (chumicro-repl · /dev/cu.usbmodem14101 · circuitpython · 115200 baud) and the four keys you might want, then sends a single carriage return to nudge the friendly REPL into reprinting its >>>:
| Key | Effect |
|---|---|
| Ctrl-C | Forwarded — interrupts on-device. |
| Ctrl-D | Forwarded — soft-reboots the runtime. |
| Ctrl-E | Forwarded — enters MicroPython paste mode. |
| Ctrl-X | Local exit — quits the TUI without rebooting the board. |
All other keystrokes pass through unchanged. The board does its own line editing — arrow keys, backspace, history all work, but they live on the device, not the host. No : commands, no Tab completion against the host catalog, no persistent history file (the device's own history scrolls inside the runtime's >>> prompt).
Choose passthrough when you need byte-exact behavior or when you're piping input over stdin (line mode requires interactive input from a TTY); choose line mode for everyday inner-loop work. --mode auto (the default) inspects sys.stdin.isatty() and picks the right one.
Tail mode for deploy follow-ups¶
from chumicro_deploy import Device
from chumicro_repl import tail, ExitCode
device = Device(
transport="circuitpython",
address="/dev/cu.usbmodem14101",
)
result = tail(device, seconds=10.0, fail_on_traceback=True)
if result is ExitCode.TRACEBACK_DETECTED:
raise SystemExit("deploy crashed on the board")
elif result is ExitCode.OK:
print("clean tail — board ran without surfacing a traceback")
tail() accepts either a chumicro_deploy.Device or a bare port path string. Output is decoded UTF-8 safely (multi-byte code-points split across reads do not corrupt the stream) and ANSI-highlighted as it scrolls past — tracebacks are bold red, safe-mode banners are yellow, hard faults are red-on-red, MicroPython soft-reboot banners are dim cyan.
The window ends when:
- The
secondsbudget elapses →ExitCode.OK. fail_on_traceback=True(default) and a traceback / safe-mode / hard-fault block is detected →ExitCode.TRACEBACK_DETECTED.- A
KeyboardInterruptreaches the read loop →ExitCode.INTERRUPTED.
Soft-reboot banners are informational and never end a tail early.
Programmatic raw REPL — ReplSession¶
ReplSession is a context manager that puts the board into raw REPL on entry and exits cleanly. Three primitives:
from chumicro_repl import ReplSession
with ReplSession("/dev/cu.usbmodem14101") as session:
# Run a block — returns stdout as a UTF-8 string.
output = session.exec("import os\nprint(os.uname())\n")
# Call a named function with literal args, parse the repr.
voltage = session.call("supervisor.runtime.usb_voltage")
# Stream-read until a regex matches. Useful for waiting on a
# board signal before the next exec.
captured = session.read_until(r"READY", timeout=5.0)
exec(code, timeout=10.0) returns stdout. If the board emitted stderr (typically because the code raised an exception), ReplSession raises ReplSessionError with the stderr block attached as the exception's .stderr attribute — raw REPL never raises a Python exception object across the wire, so surfacing it as a string is the closest host-side analog.
call(function_name, *args, **kwargs) builds a print(repr(<function_name>(*args, **kwargs))) and parses the result via ast.literal_eval. Round-trips numbers, strings, bytes, tuples, lists, dicts, sets, booleans, and None. Anything else raises ReplSessionError because the repr is not a literal.
read_until(pattern, timeout) reads bytes from the port until the regex matches the accumulated decoded text. Operates on the friendly REPL too, so callers tailing a deploy can wait for a specific signal without bouncing through raw REPL.
The session accepts a chumicro_deploy.Device, a bare serial-port path, or any object with .address and .baudrate attributes. Tests inject time (a TimeSource protocol) and port_factory (any callable returning a SerialPort protocol) so the whole context is exercised without real hardware.
Recover from session-start failures — InteractiveReplSession¶
ReplSession raises directly on a failed session-start — that's the deterministic surface programmatic callers want. For interactive use, InteractiveReplSession wraps a ReplSession with classification + a retry-loop + user-facing coaching, mirroring chumicro_deploy.RecoveringDeployer.
from chumicro_repl import InteractiveReplSession
with InteractiveReplSession(device, max_attempts=3) as session:
output = session.exec("print('hello')")
When the underlying session fails to open, the wrapper:
- Classifies the exception into a
ReplFailureKind— one ofPORT_NOT_FOUND,PORT_BUSY,PORT_PERMISSION_DENIED,RAW_REPL_UNRESPONSIVE, orUNKNOWN. - Prints the matching
RecoveryPlan(headline + ordered fix-steps) to output. - Prompts the user via prompt — bare Enter retries;
q/quit/abort/exitre-raises the last error. - After
max_attemptsattempts, the last exception re-raises.
prompt and output are injectable so you can plug the wrapper into a TUI dialog, a logging framework, or a test scripted with a queue of canned responses.
from chumicro_repl import InteractiveReplSession, classify_session_failure, ReplFailureKind
# Custom orchestrator — classify directly without using the wrapper.
try:
with ReplSession(device) as session:
...
except (OSError, ReplSessionError) as error:
kind = classify_session_failure(error)
if kind is ReplFailureKind.PORT_BUSY:
... # close-the-other-tool flow
Mid-session disconnects (after the handshake completed) do not route through this layer — those are handled by the auto-reconnect loop in tail() and run_loop() (see reconnect_seconds). Subclass ReplSessionDisconnected matters for that path; InteractiveReplSession matters for "the session never opened".
The interactive demo_repl_robustness.py example walks every scenario against a real board — happy path, port-not-found (bogus address), port-busy (open the port elsewhere first), raw-REPL-unresponsive (board stuck in interrupt-disabled code), auto-reconnect during tail (yank + replug the cable), and abort during reconnect.
Pattern detection and highlighting¶
from chumicro_repl import detect_patterns, colorize, Theme
text = "running\n" + boards_traceback_output
matches = detect_patterns(text)
for match in matches:
print(match.kind, match.start, match.end)
# Render with default colors.
print(colorize(text), end="")
# Custom theme.
theme = Theme(traceback="32") # green tracebacks
print(colorize(text, theme=theme), end="")
The StreamingPatternDetector is the primitive tail() and the TUI build on — it buffers a bounded amount of trailing context so a pattern that spans a chunk boundary still matches without growing memory on long-running sessions. Use it directly for any custom streaming consumer.
Test fakes¶
Host-side tests can drive every surface without real hardware:
from chumicro_repl import ReplSession
from chumicro_repl.testing import FakeSerialPort, FakeTime
handshake = [b"\r\n", b"raw REPL; CTRL-B to exit\r\n>"]
exec_ok = b"OKhello\n\x04\x04>"
port = FakeSerialPort(read_chunks=[*handshake, exec_ok])
with ReplSession(
"/dev/cu.fake",
time=FakeTime(),
port_factory=lambda *_args, **_kwargs: port,
) as session:
assert session.exec("print('hello')") == "hello\n"
Three fakes are exposed under chumicro_repl.testing:
FakeSerialPort— drop-in forserial.Serial. Records writes; replays scriptedread_chunks.FakeKeyboard— replays scripted keystrokes forchumicro_repl.tui.run_loop.FakeTime— deterministic seconds-domain time source.monotonic()is stable;sleep()advances the clock without a real wait.
Runtime notes¶
The raw-REPL framing is identical between CircuitPython and MicroPython — both runtimes emit the same OK<stdout>\x04<stderr>\x04> shape on the same Ctrl-A/Ctrl-D handshake. The only divergence is what a friendly-REPL Ctrl-D prints: MicroPython emits MPY: soft reboot and CircuitPython is silent. ReplSession never leaves raw REPL during operation, so the divergence never surfaces there; it's a concern only for tail() (which highlights the soft-reboot banner) and the interactive TUI (which forwards Ctrl-D and shows whatever the board prints).
Pattern detection covers:
| Kind | Source | When it appears |
|---|---|---|
| Traceback | both | An uncaught exception. |
| Safe mode | CircuitPython | Repeated crashes or supervisor reload into a broken state. |
| Hard fault | CircuitPython | A crash below the Python-exception layer. |
| Soft reboot | MicroPython | Ctrl-D in the friendly REPL or machine.soft_reset(). |