docs: add VHS walkthrough demo to README#50
Merged
Conversation
876f907 to
9264953
Compare
… prompt Two issues with the dual-pane debug orchestrator left the user staring at a screen they couldn't tell was live: 1. No initial paint. run_debug() entered its main loop with `dirty=0`, so the alt screen stayed blank until something set dirty=1 -- gdb's first chunk after `target remote :<port>` can land outside the 30ms read budget on a slow handshake, so users saw an empty alt screen until they pressed a key. Both cmd/target/openocd and cmd/target/qemu --debug share this shape; the single-pane callers (cmd/target/qemu, cmd/target/monitor) already render once before their loop. 2. (openocd only) gdb prompt buried by trailing openocd "Info" lines. gdb writes "(gdb) " to its pty, then openocd's post-attach chatter (typically `Info : Detected FreeRTOS version: (10.5.1)`) runs through the same vt100 grid and advances the cursor past the prompt. The user sees `(gdb) Info : Detected FreeRTOS...` with the cursor on a blank line below; gdb has no reason to redraw until it gets input, so the prompt looks dead until you press Enter. Fix: - Seed `dirty=1` on the first loop iteration in both run_debug() variants so the empty layout + status bar paint immediately. - (openocd) Track time of last activity on either pipe. Once gdb has been seen AND both streams have been quiet for 300ms AND the user hasn't typed yet, send a single bare newline to gdb's pty. gdb prints a fresh prompt below the noise. The empty Enter is safe: `set confirm off` is set, and `set` / `target remote` are dont_repeat in gdb, so it's a guaranteed no-op apart from the redraw. Suppressed if the user has already typed -- their characters went to gdb's stdin, and prefixing them with \n could change the command boundary. Signed-off-by: Frantisek Hrbata <frantisek.hrbata@espressif.com>
…ate text Three issues with the one-line attach hint at the top of the UART pane: - It used \x1b[2m (dim), which on attach+focus-switch made it easy to miss. Switch to \x1b[33m (yellow) so the user spots it on first glance. - The hint ended in a bare LF. vt100's LF only moves the cursor down -- it doesn't reset the column -- so after rendering the hint the cursor sat one row below at the column past the hint text. The first chip UART byte to land then started in the middle of an otherwise-empty pane, looking like a cursor parked somewhere weird until output started flowing. Use CRLF. - The text claimed `Ctrl-T r resets and shows boot logs`, but Ctrl-T r runs `monitor reset halt` -- the CPU stays halted at the reset vector, so no UART comes out until the user types `continue` in the gdb pane anyway. Reset isn't the simpler primary action for "I want to see UART output"; `continue` is. Drop the reset reference and point the user at `continue`. Signed-off-by: Frantisek Hrbata <frantisek.hrbata@espressif.com>
…ollback spawn_openocd() streams openocd's startup output (banner, JTAG probe, listening-port lines) to ice's stderr in real time so the user can watch the daemon come up and so any failure diagnostic stays visible after ice debug exits. Once the gdb listener is detected the captured prelog was thrown away, so inside the alt screen there was no way to look back at the banner -- you'd have to drop out of the dual pane to find it in your terminal's pre-alt-screen scrollback. Hand the prelog through to run_debug() and seed gdb_p.L's ring with tui_log_append() before the main loop. The banner doesn't land in the visible grid (vt100 fills the body, leaving 0 scrollback rows visible by default), but PgUp / Ctrl-T inspect into the gdb pane brings it up immediately. Ownership: cmd_target_openocd() owns the sbuf, spawn_openocd() appends, run_debug() consumes (read-only). On spawn_openocd failure the caller releases. Signed-off-by: Frantisek Hrbata <frantisek.hrbata@espressif.com>
…gdb) The settle nudge wrote a bare \n to gdb so it would redraw its prompt below any trailing openocd "Info : ..." line that buried the original (gdb) prompt. Two issues with the previous shape: 1. The nudge fired unconditionally on the 300 ms quiet window, even when gdb's prompt was the last thing on the grid (no burial). In that case the redraw added a redundant duplicate prompt one row below the real one. 2. When the prompt *was* buried, the redraw left a blank row between the buried prompt and the new one. vt100 cursor sits at (N+1, 0) after openocd's trailing CRLF; gdb's response to \n is "\r\n(gdb) ", so \r → (N+1, 0), \n → (N+2, 0), prompt writes at (N+2, 0)-(N+2, 5), and row N+1 stays empty. Fix: - Decide whether to fire by reading the vt100 cursor column. Cursor at col != 0 means gdb's "(gdb) " is the latest thing on the grid -- skip the nudge. Cursor at col 0 means openocd's CRLF is the latest -- fire. - When firing, push "\x1b[A" into the gdb pane's vt100 before writing \n to gdb. That moves vt100's cursor up onto the buried row, so gdb's \r\n echo steps the cursor onto the row that *was* the blank successor and "(gdb) " lands there instead of one row below it. Side benefit: vt100's cursor row and gdb's mental cursor row stay aligned, so subsequent readline manipulation (backspace, tab redraw) operates on a consistent terminal model. Verified on hardware: with the buried "(gdb) Info : Detected FreeRTOS..." on row N, the redrawn "(gdb) " now appears on row N+1 with the cursor at col 7, no blank gap. Signed-off-by: Frantisek Hrbata <frantisek.hrbata@espressif.com>
…ntry The previous fix routed the captured prelog into gdb_p.L via tui_log_append() so it landed in the scrollback ring. But the ring isn't visible by default -- vt100 fills the body, leaving 0 visible scrollback rows -- so the banner was reachable only by PgUp / Ctrl-T inspect. That defeats the point of preserving it inside the alt screen: users who don't know to scroll just see the banner vanish under the dual pane. Feed it through vt100_input() instead. The banner occupies the top of the gdb pane immediately, then is naturally pushed into the ring as gdb's output and openocd's post-attach Info lines fill the grid -- exactly the behavior of any other long log streamed into a viewport. On a tall enough terminal the banner stays visible alongside the rest of the session; on a small terminal it scrolls into the ring as before, just with the same discoverability path (PgUp). Drain scrolled-off rows into the ring explicitly here -- pump_pipe does this each frame, but on entry there's no read yet, so without the explicit pull any rows that scroll off during the prelog feed itself sit in vt100's bounded queue rather than in the ring. Moved the seed past debug_layout() so vt100 is at the pane's final size before content goes in. Signed-off-by: Frantisek Hrbata <frantisek.hrbata@espressif.com>
… app_main) ice debug previously launched gdb with just -ex "target remote :<port>" + the elf positional, on the assumption that this kept chip state intact for post-mortem inspection. That assumption was always false on Espressif chips: openocd's standard board files (esp32s3-builtin.cfg, esp32-wrover-kit-3.3v.cfg, ...) already issue `reset halt` during their `init` step, *before* gdb dials in -- you can see it in openocd's own log lines as "Reset cause (3) - (Software core reset)" right after attach. The chip is reset whether we like it or not. The asymmetry between "openocd resets" and "gdb thinks it's attaching to a free-running chip" produces a nasty trap. After openocd's reset-halt-and-init the chip is allowed to run again, and ~few-hundred-ms later (the wall-clock budget of ice spawning gdb and gdb dialing tcp/3333) gdb's gdb-stub-on-connect halt lands somewhere in the post-reset boot path -- almost always on app_main's prologue, because hello_world spends very little of that window outside vTaskDelay. The user then naturally types `b app_main; c`. The HW breakpoint they just armed sits at the exact PC the chip resumes from; the very first instruction fetch re-trips the match register before any instruction commits; openocd-esp32's recovery for that state is a software CPU0 reset, which puts the chip back at the reset vector; the chip runs through boot to app_main; the still-armed breakpoint fires for real this time; the user `c`s; and we loop at ~10Hz with no printf ever flushing. Fix: pre-load gdb with the same connect fragment that ESP-IDF's tools/cmake/gdbinit.cmake generates -- target remote :<port> monitor reset halt maintenance flush register-cache thbreak app_main continue The temp HW breakpoint at app_main fires exactly once on the way through boot, gdb auto-removes it, the chip is parked at app_main with no breakpoint at PC, and any subsequent `b app_main; c` works because the user's breakpoint isn't sitting on the resume PC anymore. The one-instruction auto-skip required to step *over* a freshly-installed breakpoint after the user has already moved past it is the standard gdb path and does work. The misleading port-resolution comment that referenced "attaches to a *running* chip" as the rationale for the passive port picker is also tightened: the rationale (don't toggle DTR/ RTS into ROM bootloader on the same USB device openocd's libusb just enumerated) is real, but it's not about preserving chip state -- openocd already destroyed that. Verified end-to-end on the actual chip: with the preamble, `b app_main; c` followed by repeated `c`s produces 12+ Hello world prints across multiple esp_restart cycles, real breakpoint hits at app_main, no software-reset loop. Signed-off-by: Frantisek Hrbata <frantisek.hrbata@espressif.com>
Now that ice debug pre-loads gdb with the idf.py-style preamble
(monitor reset halt + thbreak app_main + continue), the chip
parks at app_main via a clean breakpoint hit and the prompt
sequence no longer races openocd's "Detected FreeRTOS" Info line
the way it did when target remote was the only -ex. The
defensive belt-and-suspenders I added earlier --
- first_paint flag forcing a render on iteration 0,
- settle nudge that wrote a bare \n to gdb after a 300ms quiet
window when the gdb pane's vt100 cursor parked at col 0,
- cursor-up trick that pre-emitted \x1b[A so gdb's \r\n echo
landed the redrawn prompt on the originally-blank row,
- user_typed gate that suppressed the nudge once the user had
interacted,
were sized for the previous flow's race window and now produce a
visible regression: the post-preamble prompt is already clean
(cursor parked at col 7 right after "(gdb) "), but the settle
heuristic still fires often enough to draw a redundant second
prompt below the real one.
Drop the lot. The shape of run_debug() returns to what it was
before commit ecbea1a, plus the keep-from-this-branch
changes (UART pane hint colors and CRLF, prelog seeded into the
gdb pane's live grid, idf.py-style gdb preamble).
Signed-off-by: Frantisek Hrbata <frantisek.hrbata@espressif.com>
resolve_idf_arg used to take the @b{<idf>} positional verbatim (after the bare-name -> ~/.ice/checkouts/<name> shorthand) and hand the result straight to setenv("IDF_PATH", ...) and the profile config in .ice/config. When the user passes a relative path -- the natural shape inside the project tree, e.g. @c{ice init esp32s3 ../../..} from @c{esp-idf/examples/get-started/hello_world/} -- the value works fine for the directly-spawned cmake configure (which inherits the project's cwd), but esp-idf's project.cmake re-runs the toolchain file from cmake's @c try_compile under @c build/CMakeFiles/CMakeScratch/TryCompile-XXX/, where the generated @c build/toolchain/toolchain-<chip>.cmake's @c{include($ENV{IDF_PATH}/tools/cmake/toolchain.cmake)} resolves the relative path against the wrong cwd and fails with @c{include could not find requested file: ../../../tools/cmake/toolchain.cmake}. Canonicalize the resolved path through a new platform helper @c path_realpath that wraps POSIX @c{realpath(p, NULL)} on POSIX and @c GetFullPathNameW on Windows. Lives in platform/{posix,win}/io.c next to the rest of the path-aware filesystem family (getcwd_w, fopen_w, ...); the public declaration is in platform.h with the cross-platform-semantics doc. POSIX side gates the file with @c{_XOPEN_SOURCE 500} since the build's @c{_POSIX_C_SOURCE=200112L} doesn't expose @c realpath under glibc. If the path doesn't exist yet, @c path_realpath returns NULL and @c resolve_idf_arg falls through with the verbatim string -- the existing @c{<idf_path>/tools/tools.json} access() check below will report a clean error. Signed-off-by: Frantisek Hrbata <frantisek.hrbata@espressif.com>
Add demo/demo.tape and demo/demo.gif: an end-to-end ice walkthrough on ESP-IDF's hello_world example, driving tab completion, ice repo checkout, ice init, ice menuconfig, ice build (spinner-only -- Ctrl-V verbose toggle works interactively but overwhelms vhs's screen tracking when ninja streams hundreds of lines through it), ice flash over USB-JTAG, ice monitor, ice qemu, ice qemu --debug, and ice debug. Real chip + real QEMU + real OpenOCD on the host the chip is wired to. Synchronization between vhs and ice's progress-driven steps (checkout / init / build / flash) uses `Wait+Screen /<msg> done/` against ice's success line so the next Type fires only after ice has fully released stdin -- a wall-clock Sleep either wasted time or got truncated, and bytes typed while ice was still polling stdin in raw mode would get eaten. +Screen (vs default +Line) is needed because the bash prompt overwrites the success line right after it prints. The prelude clears the screen inside the Hide block so the setup-cd / PS1-export typing doesn't bleed into the first captured frame. Hoist the existing `**Experimental PoC**` callout to the top of the README so it's the first thing a reader sees, and extend it to note that the project is tested primarily on Linux -- Windows may not work as expected, or at all. Drop the prose paragraph describing the walkthrough; the GIF plus the tape file alongside it carry the same information more cheaply. Signed-off-by: Frantisek Hrbata <frantisek.hrbata@espressif.com>
9264953 to
7617b3a
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
demo/demo.tape(VHS recording recipe) + rendereddemo/demo.gif..gitignoreexcludes theesp-idf/,.ice/,build/trees that vhs creates underdemo/while recording.The walkthrough exercises tab completion,
ice repo checkout,ice init,ice menuconfig,ice build(toggling verbose withCtrl-V),ice flashover USB-JTAG,ice monitor,ice qemu,ice qemu --debug, andice debug(reset, continue, hop into the chip's console via the monitor pane, tab-completehelp, hop back, interrupt, backtrace).Recording requires vhs + ttyd + ffmpeg, an attached ESP32-S3, and a populated
~/.ice/esp-idfreference; full prerequisites and tweakable knobs are documented in the tape's header.Test plan
vhs demo/demo.taperegeneratesdemo/demo.gifon a fresh clone with the chip attached.🤖 Generated with Claude Code