summaryrefslogtreecommitdiff
path: root/docs
AgeCommit message (Collapse)Author
27 hourstools: fix membrowse edge caseshathach
28 hoursci: remove dead metrics scaffoldinghathach
28 hoursci-pinned-boards: curate the board set, esp upload via hil-build-esp, rp2040 ↵hathach
fixes The cmake job builds and uploads every pinned board to membrowse on every run, PRs and pushes alike (--identical when code did not change) - it is the single owner of that upload, as before this branch touched it. Curate the board set itself. cxd56/spresense, da1469x/da1469x_dk_pro, fomu/fomu and ft9xx/mm900evxb are no longer pinned CI boards: none sit on either HIL roster and no other CI board carries their driver, so their size tracking was build-only with no hardware behind it - documented with honest `uncovered` reasons instead of silently dropping coverage. Swap lpc15/lpcxpresso1549 -> lpc11/lpcxpresso11u68 for dcd_lpc_ip3511, lpc17/lpcxpresso1769 -> lpc40/ea4088_quickstart for dcd_lpc17_40+ohci, and samd5x_e5x/same54_xplained -> metro_m4_express; adafruit_fruit_jam now also claims dcd_rp2040. All four changed boards build-verified (device/cdc_msc, cmake), and the rp2040 group's (family, board) sort order restored. Two stale notes the swap left behind are fixed: lpcxpresso11u68 is on neither roster, so its "HIL (htpc)" claim (copy-pasted from the entry it replaced) was false, and ea4088_quickstart is a maintainer bench board noted the same way frdm_kl25z and lpcxpresso1549 were. Rename the board file to .github/ci-pinned-boards.json, with matching --ci-pinned-boards/--ci-pinned-boards-only flags: it pins the board that represents each family in CI, and membrowse history is keyed on each pinned board's <board>/<example> target names, so the set is explicitly held rather than derived. Content and target names are unchanged; the per-entry "family" key is dropped - board names are unique across hw/bsp/*/boards, so it was redundant data that could drift from the tree. build.py's resolve_ci_boards() and the coverage checker derive the family from the board dir, and entries are sorted by board name, the only key left to sort on. espressif builds have no leg in the cmake job (esp-idf is by far the slowest toolchain, and its boards are already enumerated by name for the rig) - hil-build-esp keeps its own upload-membrowse + secrets: inherit instead, and hil-build-esp-identical covers the --identical row for a no-code-change push, the same gap hil-build-esp's own code-changed gate would otherwise leave. build_util.yml's Membrowse Upload step skips any leg whose args carry --build-name, an extra -D or --cflag - a HIL variant build - since it configures with the same -DBOARD=<name> as the plain build and would otherwise fight it for the same upload target name; this protects hil-build-esp's DMA variants specifically. The step also does not scope by $EX_ARGS (kept on the Build step): scoping the upload by the PR's example filter would leave examples outside that filter with no upload attempt at all - a silent history gap rather than the --identical row the design intends. Fork PRs: GHA withholds secrets, so MEMBROWSE_API_KEY reaches membrowse_report.py empty. Master's CMake-expanded bare --api-key fell through to membrowse's GitHub tokenless auth; the wrapper now does the same by omitting --api-key entirely when the env var is empty, rather than exiting - the previous hard-exit was silently hidden behind the workflow's continue-on-error, so every fork PR lost its upload with no visible failure. Retiring ci_skip_boards let CircleCI's plain family sweep reach five rp2040 boards for the first time; two didn't build. pico_sdk fails dual/host_hid_to_device_cdc and dual/host_info_to_device_cdc - a meta-board with no board.h pin defines of its own has no PICO_DEFAULT_PIO_USB_* macros, which a dual-role rp2040 example needs for its PIO-USB host side - so each example gained a skip.txt entry naming the failure. feather_rp2040_max3421 failed host/bare_api on -Wmissing-prototypes for max3421_int_handler in hw/bsp/rp2040/family.c; it is only ever used as a same-file GPIO IRQ callback (no other caller), so it is marked static rather than skip-listing a zero-risk one-line fix. All 8 rp2040 boards now build clean, `-b <board>` and the bare `rp2040` family sweep alike. drivers_coverage_check.py type-checks `drivers` before iterating: a string previously iterated character-by-character into nonsense "X matches no driver source file" errors, and null raised an unhandled TypeError. Not fixed, flagged for a follow-up: MEMBROWSE_API_KEY reaches membrowse_report.py as a child-process argv (visible in /proc/<pid>/cmdline on the runner, even though the logged command line is redacted) rather than via the environment. Final-review fixes: the espressif Membrowse Upload docker run only forwarded MEMBROWSE_API_KEY and CI into the container, so membrowse's --github metadata detection (GITHUB_EVENT_NAME/GITHUB_SHA/...) and its fork-PR tokenless auth never saw the actual event - add GITHUB_ACTIONS/GITHUB_EVENT_NAME/GITHUB_SHA/GITHUB_REF_NAME plus a read-only bind mount of GITHUB_EVENT_PATH (containers don't inherit runner env by default; the esp Build step needs none of this, since its `--target all` never builds the *-membrowse/-upload custom targets, so its now-unused `pip install membrowse` is also dropped). check-paths' code filter also gains tools/membrowse_report.py, so a PR touching only that file exercises the real build+upload matrix (ci_select rule 2d) instead of 74 no-op --identical legs. Review fixes: generate_membrowse_sizes()'s all-examples scope (-b with no -e) now averages each file's size across the elfs it appears in instead of summing, matching metrics.py's compute_avg() semantics from the linkermap engine this branch replaced - a file linked by N examples no longer reports ~N times its real size, and the Flash/RAM columns are a real binary's size again; single-example (-e) stays byte-identical (sum, same as before). The Membrowse Upload step (build_util.yml) and hil-build-esp-identical's loop (build.yml) now strip any -e <example> pairs hil_ci_set_matrix.py bakes into a PR-scoped hil-build-esp leg's args before invoking, so a PR-scoped run still uploads every example of the pinned board rather than only the PR-selected subset - closing exactly the silent history gap the step's own comment already warned against. Also: build.py's espressif branch gates the idf.py build path on 'all' being present in build_targets rather than being exactly ['all'], so a combined --target invocation still runs the build instead of silently skipping on a fresh dir; membrowse_compare.py's layout-based bucketing unions the actual flash/ram buckets of a multi-region section's regions instead of assuming any 2+-region split is a flash+ram pair (e.g. RAM_D1/RAM_D2 now stays ram-only); membrowse_report.py's extract_defsyms() dedupes to match its own docstring. Post-validate fixes, folded in. The Membrowse Upload step threads the PR's -e filter through for BOARD ELIGIBILITY only: without it, resolve_ci_boards() ran with examples=None and still resolved the pinned board even when the Build step above had fallen back to a substitute, uploading --identical for a board that run never built. Under --ci-pinned-boards-only tools/build.py now nulls the example filter after board selection, so the upload still touches every example of whichever board got resolved - real rows for what was built, --identical for the rest. same7x earns its pin instead of assuming it: a full family sweep showed five *_freertos examples failing (no FreeRTOSConfig.h wired for same7x - skip.txt'd as family:same7x, naming the reason) and same70_qmtech's board.cmake/mk pointing at a same70n19b_flash.ld that never existed in the tree (the SDK ships none) - the board now carries its own, Q21B's script with the N19B's MEMORY sizes (512K flash / 256K ram per same70n19b.h), like the other boards with a board-local .ld. Both same7x boards now build their full example sets green under the real budget. cxd56 stays OUT of ci_set_matrix's family_list and its BSP reverts to master untouched: the same sweep showed NuttX-header failures across three examples, spresense is unpinned anyway, and a -Wno-error suppression for a family no CI leg builds silences a real diagnostic class for nothing. drivers_coverage_check.py gains two guards: a pinned family that no CI toolchain actually compiles is now a validity error (the dead-pin case the same7x episode demonstrated), and a non-dict entry in "boards" reports one clear error instead of a traceback. The spec and plan docs are updated to the shipped names (membrowse-targets.json / --board-pins / --pins-only never shipped; 71 occurrences), and stale test fixtures naming the deleted tinyusb_metrics target are repointed at examples-membrowse-upload. Validate-loop fixes, folded in. membrowse_compare.py's report_for_elf() now passes the elf's own linker scripts and --defsym values (read from its ninja build graph, exactly as the CI upload path does) to `membrowse report`: with none given membrowse falls back to its default Code/Data regions, which the layout bucketing cannot map, so `.data` lost its flash-side load image and was counted RAM-only (verified on stm32h743eval: region 'Data' without the script, ['FLASH','RAM_D1'] with it). _bucket_from_layout() also unions in the flash side for a BOTH_SECTIONS section whose layout lists only its RAM region - pico-sdk's script never lists `.data` under FLASH, undercounting raspberry_pi_pico's flash by 11.2K. membrowse_onboard.py: `membrowse onboard` checks out and `git clean -fdx`s every historical commit in whatever directory it runs, which would detach HEAD and wipe the deps symlinks in the repo root - so the wrapper runs it in a disposable worktree under cmake-metrics/, and the composed build script relinks deps and reconfigures the (also wiped) build dir before every historical build, not just the first. --build-dirs now includes the example's own directory, so a change to its sources no longer backfills as --identical. Not fixable in the wrapper: onboard applies one commit's linker scripts to every commit in the range (no per-commit override in its CLI).
28 hoursmembrowse tooling: fail loudly, dedup espressif, bucket by memory layouthathach
metrics_compare_base.py caught nothing when the `membrowse` CLI was absent, so the default engine died with a bare traceback after both builds had already run; it now points at `pip install membrowse` or --engine linkermap, documented in the skills. membrowse_report.py's ninja query returned '' on failure, starving the linker-script and --defsym extraction so membrowse silently reported default Code/Data regions and exited 0; it now exits with the failing invocation, and likewise when a successful query yields no linker script without --ld. membrowse_onboard.py passes the extracted --ld-scripts/--def so a backfill matches what CI uploads. Stale bare --ci/--combined examples in the tool and in CLAUDE.md are corrected. espressif was compiled twice per run - once by hil-build-esp for the rig, once by the cmake job's esp-idf leg - so it leaves ci-boards.json, the cmake matrix and ci_set_matrix's family_list (which also removes a never-run CircleCI leg that would have built all 15 espressif boards), and hil-build-esp carries the membrowse upload instead. A separate job, gated on the opposite condition and with no downstream dependents, does the --identical upload on no-code-change pushes without letting the HIL test jobs reach the rig. Flash-vs-RAM bucketing stops guessing from section names. TinyUSB places code and buffers in RAM under names no prefix list can enumerate - .time_critical.tinyusb, NonCacheable, .fast, .ccmram - so RAM deltas read as 0 on exactly the boards that do it. Cross-reference each symbol's section against every memory_layout region's sections list instead: present in two regions means a flash load copy and a RAM run copy (both budgets), one region is classified by region name or, if that is unrecognized, by the ELF section type membrowse already derived from sh_flags. The name table survives as a documented fallback, extended with the vendor flash and RAM section names verified in this repo's linker scripts. Note membrowse 1.2.9 reports every linker-script-derived region's type as UNKNOWN, so the region name is the only region-level signal it surfaces.
28 hoursci-boards: one curated board list for CI, membrowse and HIL coveragehathach
The pinned-target file becomes .github/ci-boards.json - "pin" reads as GPIO pin in this repo - and with it the flags (--ci-boards, --ci-boards-only), the resolver (resolve_ci_boards()) and the JSON's own key (boards). It is now the only board-curation mechanism: tools/build.py's ci_skip_boards/ci_preferred_boards are gone, with samd2x_l2x and stm32h7 pinned to preserve today's CI picks, and resolve_ci_boards() applies the -e buildability filter so a scoped PR cannot pick a board that builds none of the selected examples and go green having compiled nothing. Curate the set itself: RP2350 boards (raspberry_pi_pico2, adafruit_fruit_jam for hcd_pio_usb) replace feather_rp2040_max3421, add max32666fthr, align the lpc55/nrf pins with rig boards, and drop the nuvoton trio, msp432e4, broadcom_64bit, f1c100s, gd32vf103 and mm32 - dwc2 stays covered, the rest move to uncovered with reasons. The checker moves to tools/drivers_coverage_check.py and reports coverage for both consumers without failing on gaps: membrowse boards (documented uncovered -> INFO, undocumented -> WARNING) and the HIL rosters (INFO), mapping boards to drivers through ci_select's rule-3/4 machinery including its role filter, so an hcd driver only counts host-role rig boards. Validity errors - unknown names, a driver both pinned and uncovered, an hcd claim on a board with no host example - still exit 1. Its chdir now scopes both cwd-relative calls, so the checker works from any directory, and the hook watches the inputs it actually reads.
28 hoursmetrics: replace the linkermap CI pipeline with membrowsehathach
Membrowse becomes TinyUSB's size-analytics system, in CI and locally. .github/membrowse-targets.json names the boards CI builds per family and the drivers each covers; a checker enforced by pre-commit fails when a dcd/hcd driver (plus ehci/ohci) is neither claimed nor documented in `uncovered`, validates board and family names against hw/bsp, and requires a board claiming an hcd driver to build at least one host/dual example. tools/build.py resolves that file into the build matrix and gates uploads on it. Locally, tools/membrowse_compare.py diffs `membrowse report --json --all-symbols` per source file, keyed on object_file (membrowse 1.2.9 truncates source_file to a basename), and metrics_compare_base.py uses it as the default engine, with `--engine linkermap` as the legacy map.json fallback (still required for --combined/--ci). A guard errors instead of writing a silent all-zero table when the filters match nothing. linkermap stops running on every build: the POST_BUILD hook goes, the explicit <target>-linkermap target and a new examples-linkermap aggregate stay, and family_add_linkermap() no-ops when the dep is not fetched. CI loses the whole linkermap pipeline - the code-metrics job, the sticky PR size comment, release metrics assets, per-family artifacts, metrics_pair_compare.py, the membrowse-onboard workflow and CircleCI's dead tinyusb_metrics lane. The cmake job builds the named boards (esp-idf included, membrowse wired into the IDF docker path via family_initialize_project and MEMBROWSE_LD_OVERRIDE) and uploads only those, keeping the --identical path for code-unchanged runs. ci_select follows: the size tooling no longer runs in any CI build, while the board list decides what a family's legs compile and so forces a full matrix. Adds the espressif, same7x, cxd56 and f1c100s families the board list needs, each build-verified. The upload itself moves out of cmake: family_add_membrowse()'s ~90-line inline bash becomes tools/membrowse_report.py, carrying ninja linker-script extraction with recursive INCLUDE resolution, --defsym collection, map-file detection, --identical fallback and upload composition. The API key is read from the environment at run time, passed via argv and redacted in logs - nothing is baked into build.ninja at configure time, which the CMake-expanded $ENV{MEMBROWSE_API_KEY} used to do. tools/membrowse_onboard.py wraps `membrowse onboard` for history backfill, deriving the build script, elf path and the CI-matching target name (<board>/<basename>) so a backfill cannot land under a name CI never uploads to; it refuses to start on a dirty worktree, since onboard checks out past commits in place. A membrowse skill documents local reports, size diffs, board-list maintenance, uploads and backfill; the code-size skill records the new default engine and its linkermap fallback. Three follow-up handoffs capture what was deliberately split out: full linkermap removal, membrowse --combined support, and the dead build_filtered plumbing.
28 hoursdocs: add rework-metrics design spec and implementation planhathach
Membrowse becomes the single CI size-analytics system: the linkermap pipeline (tinyusb_metrics, code-metrics job, sticky comment, release assets) is removed, uploads are restricted to a pinned board set covering every dcd/hcd driver, and linkermap stays local-only for the code-size skill. The plan breaks the spec into eight reviewed tasks.
2 dayshil: add ea4088_quickstart with an RTT consolehathach
The board's probe is an LPC-Link2 with no VCOM, so its console is RTT. Two host tests could not run that way: test_host_cdc_msc_hid and test_host_msc_file_explorer opened the flasher's VCOM directly instead of going through open_board_console(), and the config gate rejecting is_cdc/is_msc fixtures on "logger": "rtt" boards existed only to keep them from dying mid-run on a port that does not exist. Route both through the console abstraction and drop the gate. The reset ordering differs per console -- RTT owns the probe so the board must be reset before the console opens, a VCOM survives the reset so it opens first -- and that rule now lives in open_console_reset() instead of being spelled out at each call site. The roster entry runs device, host and dual: USB1's Type-A drives a hub with a CH340 (TX-RX shorted, for the cdc echo) and a thumbdrive, while USB2 keeps serving the device tests. BUFFER_SIZE_DOWN=128 because that echo writes up to 64 bytes at once and SEGGER's ring keeps one byte free, which the 16-byte default cannot hold; 64 measured unreliable, 128 reliable. Measured on the rig: 19 passed, 0 failed, 0 skipped. usbtest 30/30, host MSC reads 682 KB/s, device CDC/MSC ~510 kB/s (full speed behind the rig's hub).
3 daysMerge pull request #3884 from hathach/claude/codex-collaborationHa Thach
Share Claude harness with Codex
3 daystools: share Claude harness with Codexhathach
4 daysdocs: drop the pr3840 no-boards handoff, both halves are donehathach
The doc half landed in this branch. The test half it asked for already exists: NoBoardsExitRespectsFreshness and TheNoBoardsCallSiteIsWired in test/hil/test/test_hil_report.py (375e20090) cover the fresh and --accumulate no-boards exits through mark_report_no_boards.
4 daysskills, docs: address review on the harness audithathach
The WCH lock header path sat across a code-span line break and rendered with a space in it. The pool-check reporting list had dropped the scan-mode disclosure the same file requires. The pr3840 handoff still described the no-boards bullet as unfixed; it now records the doc half as landed and keeps only the missing test.
10 daysdocs: msc-host TUR retry handoff, split out of this PRhathach
10 dayspico2_etm_trace: RP2350 board on the MIPI-20 ETM trace carrierhathach
Board files for the trace carrier (console GP12/13, LED GP10, I2C GP8/9, PIO-USB host on GP20, all retargeted in board.cmake so the SDK defaults cannot mux a trace pin), compile-time trace pin-conflict checks, the measured DBGPAUSE rationale, Ozone project, and skill/docs updates. Trace validated at the stock 150 MHz (75 MHz TRACECLK, +1 ns sampling): zero overflow through a 15 s throughput soak; V2 probe ceiling 120 MHz.
10 daystest/hil, docs: move the containment history into the design dochathach
The modules were 21% comment, much of it review-cycle argument rather than guidance -- _kill_kids stated 'descendant by construction, no argv check needed' twice, eight lines apart. Deleting such comments outright makes maintenance worse: the next reader simplifies the thing the comment was defending. So the history moves to the 2026-07-30 fleet-wedge design doc, which gains a trim addendum recording what was removed, what was deliberately kept, and the rule that decided each -- the CI ceiling bounds how long a run burns, and does nothing about state that outlives it. One comment was not merely long but WRONG: the report wipe carried 'The unlink is DEFERRED to inside the pool try/except below', which is the opposite of what the code does -- it sits before Manager() with its own comment explaining why. That is the failure mode this pass is about, so it is deleted rather than reworded. Kept everywhere: citations that refute a plausible wrong reading. That usb_lock_device_interruptible is why the readers are killable, that usblp_mutex is driver-global, that rawmidi honours O_NONBLOCK where usblp does not. Two follow-ups are retired with them: pr3803-hil-blindness-reporting.md (there is no blindness to report any more) and pr3803-usbtest-recovery-reserve.md (the reserve is derived now). Kept: pr3803-flasher-recover.md, which PR #3832 implements, plus pr3803-pci-rebind-stranding.md and pr3803-hil-iar-rerun-spec.md, both independent of this work.
10 daystest/hil: drop the sysfs blindness subsystem and derive the recovery reservehathach
Two layers whose cost was a contract to reason about rather than an outcome. SYSFS_UNKNOWN was a three-valued return five files had to keep apart, and misreading unknown as absence was silent: a healthy board reported as a firmware regression. What it guarded is real -- `serial` is served by usb_string_attr, which takes usb_lock_device_interruptible (v6.12.96 sysfs.c:141-143), the same lock a wedged usbfs ioctl holds -- so the BOUND stays, on every caller by default. usb_scan reads `serial` on every device matching the VID, and hil_lock's controller_of does that on essentially every board, so one wedged DUT would otherwise stall every worker, not one. What goes is the third value. read_sysfs now returns str or None, and the question the third value existed to answer is asked directly instead, by two predicates that say which question they answer: sysfs_stranded() is process-wide and sticky, for hil_pool_check's footer ("could anything here be the tool losing sight of healthy hardware?"), and path_stranded(path) is per-device, which is what usbtest needs to tell a DUT whose `serial` is held under device_lock from one that genuinely left the bus -- that difference decides whether it performs driver-registry writes that take the uninterruptible device_lock. Gone: _SysfsUnknown, SYSFS_UNKNOWN, sysfs_blind, sysfs_blind_note, note_sysfs_strand, the cross-process blindness publishing and its report banner, usb_scan's (list, bool) return, usbtest's inconclusive abort, _blind_note's slot in the result tuple, and bounded_open, whose last caller went in the previous commit. The strand memo is rewritten around the one invariant that makes it safe to reuse: it is keyed by the path's kernfs inode, captured BEFORE the read. A busport does not change when a board returns to the same physical port, so a path-only blacklist outlives the wedge and hil_pool_check's own recovery flow -- reset, reflash, wait_device polling that busport -- would never look at the board again. A re-enumeration destroys the kernfs node and makes a new one, so a changed inode is the all-clear. Two ceilings bound different things: per path (_PATH_STRAND_MAX) for a board that flaps while still wedged, and per process (_STRAND_MAX) as a backstop against RLIMIT_NOFILE, counted per PATH rather than per reader because hil_pool_check runs four poll threads over one bus. A board the pool guard never reached is now reported as run-aborted rather than pool-timed-out, and outranks a stale board-locked cell for the same reason the pool-timeout cell does. Both predicates answer conservatively where they are consulted before something irreversible. path_stranded() covers the paths read_sysfs answered None for WITHOUT reading -- past _STRAND_MAX it declines to start another reader, and vouching for a path nobody looked at hands usbtest's fail-CLOSED guard a fabricated all-clear, running remove_id/unbind against a wedged device. usbtest's startup lookup carries the same caveat hil_test's absent arm already did, because its stderr is relayed verbatim into the report cell. strand_note() survives the removal for the same reason master had it: every caller that can say "not found" needs the same sentence, and the one site left to re-invent it got missed -- a wedged-but-enumerated printer was reported as an enumeration failure, sending a maintainer after firmware. The two predicates are not interchangeable, and usbtest needs both. Its per-case verdict is per-DUT -- a peer that stranded at case 2 must not make our board report wedged at case 29 -- but the finally block's cleanup is process-wide: remove_id plus an unbind of EVERY interface under the driver, including that peer's, each taking the uninterruptible device_lock. So the verdict uses path_stranded() and the global cleanup stays gated on sysfs_stranded(). USBTEST_RECOVERY_BUDGET was an independent 250s that could not actually contain the ladder it reserved for, which is why usbtest.py carried a _time_left() gate re-deciding before every step -- with a bare '- 35' for costs paid downstream that nobody could re-derive. Between them the two produced a recovery that skipped its own steps for most real hangs. The reserve is now derived from the bounds usbtest itself declares, per flasher and per target: a probe reset, a reflash, and the Rescue-DP POR plus retry a wedged RP DAP needs, plus the settles and hil_util.REAP_GRACE for each bounded step. The Rescue-DP legs are openocd-only and gated on the RP target cfg, and a stub reset is screened out, so the reserve tracks each board's real ladder instead of one fleet number: 390s for the two RP boards -- whose ladder the old 250 could not contain, which is exactly why the gates skipped their steps -- 190s for the other seventeen probe-reset boards, and 150s for esptool and lm4flash, whose reset is a no-op. Changing a bound in usbtest moves the reserve with it, and a unit test asserts it covers the ladder. With the room actually reserved, the child runs the ladder straight through: recovery_steps, _time_left, the three per-step gates and the parsed-but-unused --outer-timeout are gone. What stays is what decides outcomes -- the convoy_safe gate, reset-before-reflash, the no_op screen so a stub that resets nothing is not claimed, and wedged_pids() as the arbiter, because a clean flash only proves the probe wrote the MCU. hil_util.py 616 -> 514 lines.
10 daysAdd RTT console/capture tooling (tools/rtt.py), rtt skill, and HIL harness ↵Ha Thach
support (#3853) Promote SEGGER RTT from an inline debugging technique to a standalone skill backed by one stdlib-only implementation in tools/rtt.py: a CLI and importable module for console/capture over J-Link (RTTTelnetPort) and OpenOCD (rtt server) probes, with probe selection by serial or VID:PID, control-block address via --elf or --addr, bidirectional console, post-mortem ring dump, and --reset-before-attach for boot-time capture. The HIL harness reads a board's console over RTT when its probe has no VCOM ("logger": "rtt" plus a LOGGER=rtt variant define), covering device_info, pool-check aliveness, and CI wiring. Validated on 22 boards across both backends; 26 unit tests run in pre-commit.
13 dayshil: make hil_report.md a rendering of hil_report.json (#3840)Ha Thach
hil_report.json and hil_report.md were written independently. Four writers produced the markdown and three wrote no JSON at all -- and those three are the paths where a run died, so a JSON consumer saw nothing exactly when it mattered: the per-board verdicts an agent hands back reported the whole fleet as "no report row" while a human read the real story from the markdown. Every writer now goes through render_report(), so a table can never contain something the JSON does not. The document gains `scope` (a three-board PR run and a full run that lost 24 boards were indistinguishable) and `caveat` (how the run ended). `banner` carries rig health across an --accumulate retry; `caveat` records how a run ended and must not -- conflating them made a clean retry publish an abandonment that never happened. helper/hil_report.py owns the document end to end, dissolving the import cycle that forced write_timeout_report to compose its own markdown and removing a duplicate cell classifier kept in sync by hand. hil_summary.py is deleted; its CLI moves there. hil_ci.sh uploads the sidecar so a remote --accumulate has a merge bas
13 daysci_select: fix the membrowse test's env dependence, and stop HIL unit tests ↵Ha Thach
taking the rig (#3846) test_the_upload_board_can_diverge_from_the_built_board called get_family_boards without ci=True, so it pinned the developer's set, not the runner's: the CI skip lists move the one-first pick on three families. It held locally and went red on its first CI run. Pass ci=True, as _prune_buildable already does, and pin the runner's twelve. Rule 2 is a bare test/hil/ prefix, so the harness's own unit tests booked the full 27-board rig for diffs that cannot reach it. Carve test/hil/test/** out to rule 1b, beside test/{fuzz,unit-test}/**; the harness itself is untouched. A test pins that directory's file list, so anything added there that the rig does read fails rather than silently skipping hardware. Rule table updated in the spec and its carbon in the docstring.
13 daysci: an empty selection must build nothing, plus selector follow-ups (#3845)Ha Thach
ci: an empty selection must build nothing, plus selector follow-ups A PR whose build axis legitimately selected nothing rebuilt everything. build.yml reads .build.families twice - as a |-joined regex, and implicitly as "is anything selected" - but tested only -z "$FAMILY_REGEX", which an empty list and a charset-rejected one both satisfy while meaning opposite things. ci_set_matrix had already returned the correct all-empty matrix; the fall-open branch discarded it. #3842 and #3840 each spent 74 cmake legs on it. Branch on the two cases instead, rename FAM_* to FAMILY_*, and cover the block with a test that extracts it from build.yml and executes it - it had no test at all, which is how this shipped through two merges. Follow-ups to the same machinery: glob.escape the repo root at five sites, so a checkout path containing [ or * stops failing closed; drop the ci-full label, read after the matrix was already computed and so never functional; delete 13 mcu:MKL25ZXX / mcu:SAME5X skip tokens matching no board; carry the rule table in the module docstring, guarded against drift; and pin six selection behaviours a mutation pass proved untested. Cut the selector's cost 1.8x (26.0s -> 14.6s) with 0 divergences over 260 paths, and stop scoping the membrowse upload by the PR example filter.
2026-08-21ci_select: classify the 254 files that were reaching rule 17hathach
Rule 17 (unclassified -> full on both axes) is the fail-open net for paths nobody anticipated, and it must stay that way: a wrong `full` costs runner minutes and is visible in the run, a wrong `empty` costs a merged regression and is invisible. But nothing in the tree should REACH it, and 254 tracked files did. The cost was real. PR #3842 changed a skill, a README and .gitignore; .gitignore matched no rule, so both axes went full and 74 cmake legs span up runners to do checkout + toolchain + get_deps before skipping the build, plus the whole 30-board rig. Three changes, none of which touch rule 17 itself: 1. _META_RE - repo metadata and tooling no Build step reads: .gitignore, .gitattributes, .clang-format, .codespellrc, .pre-commit-config.yaml, .readthedocs.yaml, .PVS-Studio/, .idea/, sonar-project.properties, the packaging manifests, CMakePresets, udev rules, test/{fuzz,unit-test} (their own jobs build those), the non-build .github/ files, and the tools/*.py scripts no build invokes. Deliberately NOT included, and still full: .circleci/**, .github/workflows/build*.yml, .github/actions/**, .github/scripts/**. The line is "does a Build step read this", not "is it source". 2. Rules 15 and 16 now match what they already claimed. Row 15 names examples/<role>/CMakeLists.txt and the regex never had it; row 16 says tools/build*.py but anchored tools/build\.py$. Both got the right answer only because rule 17 caught them on the way past. Also names their siblings - family_support.mk, family_rules.mk, src/CMakeLists.txt, src/tinyusb.mk - and .circleci/**, which generates the whole CircleCI matrix and was in no row at all. 3. src/typec/** gets row 12b. It is listed unconditionally by both build systems but its body is `#if CFG_TUC_ENABLED`, which only examples/typec/power_delivery sets - the same shape as the class rule, so the same answer: the examples that enable it (stm32g4 and stm32u5 after the buildability prune), and nothing on the rig, which runs no typec test. It was force-fulling 82 families and all 30 boards. TestNoTrackedFileIsUnclassified walks every tracked file and asserts none reaches rule 17, on both axes - 254 -> 0. Verified it fails when a new unclassified path appears. That turns 17 into what it should be: unreachable for anything in the tree, so it fires only for genuinely new shapes, and the author is told to write the row rather than letting the fall-through pick an answer for them. test_full_paths used sonar-project.properties as its stand-in for "unclassified"; that is now metadata, so the case moved to the new test_repo_metadata_is_not_a_build_input, with test_the_build_machinery_is_still_full pinning the other side of the line.
2026-08-21ci: fix nine ways the selection under-selected or mismatchedhathach
Every one of these dropped coverage silently - the worst failure mode here, because the PR still goes green. Found by review, each reproduced first. Selection rules: * class_macros derived the config macro from the class DIRECTORY, so a change to src/class/midi/midi2_device.c selected the midi_test examples (which do not compile it) and never examples/device/midi2_device (the only one that enables CFG_TUD_MIDI2, and the only one that does). The file's own macro is unioned in where it differs - union, never replace: over-selecting costs a build, under-selecting merges a break. * the ${FAMILY_MCUS} fallback added for espressif fired on any family whose _family_mcus came back empty, and _cmake_sets is if()-blind and keeps the FIRST definition - so mcx/frdm_mcxn947 answered MCXA15, a token six examples' skip.txt names, dropping 12 firmware images CMake builds. Limited now to families that never spell set(FAMILY_MCUS ...) at all. * lib_examples read only an example's top-level CMakeLists.txt/Makefile; host/msc_file_explorer_freertos names lib/embedded-cli in src/CMakeLists.txt and survived by luck. The whole example tree is scanned. (SEGGER_RTT and rt-thread still resolve to nothing: all three references sit inside a LOGGER=rtt guard no CI build sets - the documented ruling, not a miss.) * get_family_boards applied ci_skip_boards/ci_preferred_boards only under GITHUB_ACTIONS/CIRCLECI, so the selector answered differently on a laptop than on a runner; _prune_buildable forces CI semantics. Its one-board pick also abandoned the whole preferred list when entry one could not build the -e set, and asked skip_example without the build's -D tokens. * _config_enables and lib_examples still read with the locale encoding - under LC_ALL=C the selector tracebacked on three tracked tusb_config.h files. The whole selector and its suite run clean there now. Workflows: * the Membrowse Upload step omitted $EX_ARGS, but --one-first now picks the board from the -e set, so it configured a different, empty build dir and uploaded --identical for a board never compiled. It takes $EX_ARGS for the BOARD; the target stays the aggregate, which has no DEPENDS and still records every example. * blanking FAM_REGEX reset only build_filtered, leaving the build scoped while code-metrics took the UNSCOPED branch and diffed a 1-family run against the full averaged baseline. All three drop together now, as CircleCI's fall-open does. * CircleCI's EX_ARGS had no character screen and is used unquoted, and its code-metrics job still exit 1'd on an empty metrics set - which a scoped build makes a legitimate outcome. * a `ci-full` PR label now turns the scoping off for one PR. A selector bug under-selects silently, and without a label the only ways back to a full matrix are accidental. Performance, since the selector gates every other job: family.cmake texts are read once rather than per changed directory (a 6,000-file dep bump re-read 84 files 99,892 times) and _scrape_mcu is cached: 2.2s -> 0.29s there, 0.8s -> 0.33s on a class diff. Tests: a drift guard for hw/bsp families absent from ci_set_matrix.family_list (they select zero legs now, where they used to ride the full matrix); the rule-4 port test asserted a SUBSET, which set() satisfies, so it could not fail on the empty selection it exists to catch; the GITHUB_ENV guard test counted a SUM of two guards. Drops metrics.py's --only-examples, which nothing called, and applies the TOTAL scrub to the by-example branch that skipped it.
2026-08-21docs: record the CI selection design and its planhathach
The binding rule table (17 rows x 3 answer columns), the measured effect per PR shape, and the reasoning behind the parts that look surprising: why empty means empty, why hw/mcu and lib are rules rather than full-matrix paths, why get_deps.py is diffed as data, and which build system is the reference. The plan is the task-by-task record of how it was built, kept as the origin trail.
2026-08-21ci: scope the build matrix and the HIL run to what a PR affectshathach
Every PR built all 74 legs (2494 example builds on GHA cmake alone) and flashed all 30 rig boards, whatever it touched. One classifier now walks the PR diff twice and answers three questions: which families to build, which examples per family, and which boards run which tests. Fail-open throughout - anything no rule classifies, any exception, any unusable output falls back to the full matrix, and a master push always builds everything. test/hil/helper/hil_select.py moves to tools/ci_select.py: it is no longer HIL-only, and tools/ is where the build side can import it. test_hil_select.py follows it as test_ci_select.py. Rules (docs/superpowers/specs/2026-08-19-ci-build-family-filter-design.md holds the full table): a port selects the families whose family.cmake references it, and its role - a dcd change skips host examples and vice versa; a class selects only the examples whose tusb_config.h enables its CFG_TU[DH]_ macro, following cross-class includes; an example selects itself; hw/bsp selects its family or board; hw/mcu and lib select whoever references them. CMake is the reference for all of it - make follows whatever cmake decides, family.mk is never scanned. Empty means empty (maintainer ruling): a rule that classifies a path to nothing selects nothing. Ports no family references, classes no config enables, libs no example builds and hw/mcu paths that resolve nowhere are all real - nothing compiles them, so nothing can validate them, and the master-push build is the net. Structural tests pin each such case with an explicit allowlist, so the day one stops being empty it fails pre-commit instead of silently narrowing CI. Per-example builds: build.py grows a repeatable -e, resolved against the targets CMake actually registered and batched into one `cmake --build --target a b c`. build_utils mirrors CMake's family_filter (the whole FAMILY_MCUS list, ${...} and string(TOUPPER ...) resolved) for the cmake side, while the make side keeps master's algorithm verbatim - the two build systems answer differently and a shared answer breaks lpc54's make link. hil-build gains this even on a full selection: 1702 example builds become 515. Transport: the selection travels as a file, never an argv or env var - a mass-sweep diff selects 261 KB against a 128 KiB exec limit, and E2BIG would fail the step before its own fallback could run. CircleCI carries the example map inside the generated config (pipeline parameters cap at 512 chars), swapped into the parameter defaults by sentinel match, and drops the scoping wholesale if that rewrite fails. Every PR-derived value written to $GITHUB_ENV/$GITHUB_OUTPUT is character-screened. Code metrics follow the scoping: metrics.py emits per-example totals, and metrics_pair_compare compares the (board, example) pairs present on both sides instead of a scoped run against a full-matrix average. The selector's own suite gates it in both providers: a selector that exits 0 with valid-but-wrong JSON is the one failure fail-open cannot catch, so a red suite means the full matrix.
2026-08-21get_deps: correct two family tokens that matched nothinghathach
get_deps matches a family token against a requested family name verbatim (`f in deps_optional[d][2].split()`), so a token naming no hw/bsp directory makes its entry unreachable: hw/mcu/allwinner said 'fc100s'; the family is hw/bsp/f1c100s, and f1c100s/family.cmake sets SDK_DIR to ${TOP}/hw/mcu/allwinner/f1c100s hw/mcu/sony/cxd56/spresense-exported-sdk said 'spresense' (the SDK's name); the family is hw/bsp/cxd56, whose family.cmake points SDK_DIR at it `python3 tools/get_deps.py f1c100s` and `... cxd56` now fetch the SDK each of those families builds against; before, both printed "no additional dependencies found". docs/reference/dependencies.rst is generated from deps_all by tools/gen_doc.py, so it is updated to match - column widths are unchanged (the widest cell is lib/CMSIS_5's, untouched) and every row was cross-checked against deps_all.
2026-08-21docs: add hardware-in-the-loop rig referencehathach
Document the ci and hfp HIL rigs in enough detail to reproduce one: bill of materials with photos, BIOS/IOMMU and vfio-pci passthrough on the Proxmox host, the Renesas uPD720201 firmware install, the guest software and permissions, the one-hub-per-root-port USB topology rule and the per-box split of probe and DUT hubs, how CI drives the rigs, and the operational gotchas. The attached-board table is generated from test/hil/tinyusb.json and test/hil/hfp.json by tools/gen_doc.py into docs/reference/hil_boards.md, which the page includes. Sphinx excludes that partial so it is not also built as an orphan document. Also exclude docs/superpowers/ from the Sphinx build: it holds internal plans, specs and handoffs rather than published documentation, and since nothing references them from a toctree each emitted "document isn't included in any toctree" -- 26 warnings in total, so build_doc.py -W could never pass. It now does.
2026-08-20docs: name the report-unification handoff after its PRhathach
The doc carried its own rename instruction for when the branch gained a number; the branch is PR #3836.
2026-08-20docs: hand off unifying the HIL report's two artifactshathach
Four writers produce hil_report.md and three of them write no JSON - the no-boards exit, the pool-guard fallback and _abandon_exit's text prepend. Those are exactly the runs that failed, so hil_summary.py, which builds an agent's per-board verdicts from the sidecar, sees nothing while a human reading the markdown sees the real story. The scope note is markdown-only too, so a three-board PR run and a full run that lost 24 boards are indistinguishable in JSON. Five tasks: put scope in the sidecar, render the markdown from the document, give the two early-exit paths a document, make _abandon_exit set a caveat field instead of prepending to a file it did not write, then pin the invariant that re-rendering the JSON reproduces the markdown byte for byte. Split out because it is a hil_test.py reporting refactor, and the abandon path runs while the interpreter is being torn down - it deserves its own review.
2026-08-20docs: spec, plan and outcome of the .claude instruction-surface audithathach
Nothing checked the agents, workflows and skills against the code they describe, and the surface had drifted into stating incompatible rules. This records the protocol that found the defects and what it cost. Method: parallel subagents extract every falsifiable claim into JSONL ledgers; a validator re-reads each cited line and rejects any ledger whose quoted text is not there, so an extractor that paraphrases or hallucinates fails a script rather than reaching the verification queue - 1,387 claims, zero such failures. Verification runs only in the main session, and the same gate pointed at `citation` then checks the verifier's own work. Hard-earned evidence is source of truth: code is authoritative about code, experience about hardware, so claims get a fourth verdict, EARNED, and "no backing found" is never grounds for deletion. All 1,387 claims carry a verdict; the behavior sweep deliberately never emits CONFIRMED from a token match, because finding a claim's vocabulary proves presence, not truth. Every real defect came from cross-document comparison - none from any mechanical pass. A path-existence lint was built, measured (11 flags on the audited tree, all false positives, and the target defect is lexically identical to correct text elsewhere), and rejected; recorded so nobody rebuilds it.
2026-08-18examples: document and work around the i.MX RT and LPC55 USB erratahathach
ERR050101: while an isochronous IN endpoint is active, an IN token addressed to that same endpoint number on ANOTHER device sharing the host can silently unprime one of this device's OUT endpoints - control, bulk, interrupt or isochronous alike. NXP states it cannot be detected by software and raises no interrupt, so the endpoint simply stops answering and the transfer never completes. The workaround is a uniqueness requirement rather than a particular number: the isochronous IN endpoint must not share its number with any IN endpoint in use on the bus. One family-wide constant therefore defeats it, since two affected boards on the same hub then pick the same number and each becomes the other's aggressor. CFG_TUSB_MIMXRT1XXX_ERRATA_ERR050101 is set only for the parts whose errata list it - RT1015, RT1020, RT1024 and RT1050, where it is marked no fix scheduled, plus RT1060 and RT1064 rev A - so RT1010 and the RT11xx family keep the ordinary number and cannot collide with an affected board beside them. Several affected boards on one hub can still be given distinct numbers with -DEPNUM_ISO_IN. The guard covers every example that has an isochronous IN endpoint: audio_test, audio_4_channel_mic, uac2_headset, cdc_uac2, usbtest, video_capture and video_capture_2ch. The video examples move the endpoint only when streaming isochronously, since the bulk configuration is unaffected, and video_capture_2ch takes two numbers because it has two streams. The macro name follows CFG_TUSB_RP2_ERRATA_E2/E4/E15 already in tree, and its is fixed, and which cannot be told apart at compile time - a way to define it to 0. device_issues.rst records ERR050101 against every affected part with a link to each errata sheet, and adds the LPC55S2x USB.3 speed-detection and USB.5 isochronous IN entries, neither of which TinyUSB works around. The branch's design notes are included under docs/superpowers. Verified: 340 wedge-free runs on mimxrt1064_evk, which previously wedged within hours, and the macro resolving to endpoint 0x87 on mimxrt1064_evk against 0x83 on mimxrt1010_evk and stm32f407disco.
2026-08-18docs: hand off follow-up work as per-PR planshathach
Records the convention in CLAUDE.md -- deferred work is a SEPARATE scope that deserves its own PR, written by another session, so it is handed off as a writing-plans doc in docs/superpowers/followup/pr<NNN>-<topic>.md rather than accumulated in the PR that found it. Five handoffs from #3803: flasher_recover (convoy-safe recovery for J-Link boards, seven validated on the rig), the blindness reporting gaps, the usbtest recovery reserve, the IAR re-run spec, and the pci-rebind stranding question. Each carries what is already established with its citations and measurements, what remains, and why it was split out. One doc per follow-up, not one per PR: a per-PR file invites unrelated work into the same document and rots as a unit.
2026-08-18skills, docs: rewrite USB recovery from the live incidentshathach
Two things the rig taught us that the old guidance got wrong. A usbfs ioctl wedged in D state cannot be freed on a running kernel. It holds the device lock, so usb_disconnect() blocks behind it; reboot(2) walks device_shutdown() and takes the same lock, so every userspace reboot stalls too. Only sysrq b (emergency_restart, which skips device_shutdown) or hypervisor action clears it -- all cited to the kernel source. The recovery ladder is generic across rigs now (ci.lan, hifiphile, a bench PC) and ends at hypervisor escalation only where host access exists. Two claims are corrected outright: JLinkExe is NOT convoy-safe, and a park-flash cannot free a device-lock owner. The hil skill's banner list is what an operator agent matches a report against, so it enumerates the banners that actually exist, including the D-state note -- which is explicitly NOT a wedge, since a healthy in-flight testusb is uninterruptible for most of every case and a concurrent CI battery would otherwise turn a clean run red.
2026-08-18test/hil, ci: contain a wedged USB stack instead of stranding the runnerhathach
A wedged USB device used to take the whole HIL run with it. Every worker that touched the poisoned node blocked uninterruptibly, the pool could not be joined, map_async discarded every board's result, and the job ran to the GitHub ceiling with no report at all -- while the self-hosted runner's single job slot stayed occupied and every queued job waited behind it. Bound the calls a worker makes itself. read_sysfs, bounded_open and run_cmd all answer within a wall clock; read_sysfs distinguishes "absent" from "unknown", because a blocked read is not evidence of absence, and caps stranded readers at four (each costs a thread and an fd for the life of the process) after which the worker declares itself blind. mtype, the gio unmount, the libmtp session and the arecord/iperf reaps go through those bounds; the MTP session runs in a disposable subprocess, since libmtp's ctypes calls block unkillably in D state. Bound the run. A pool guard (HIL_POOL_TIMEOUT, 60 min) fires before any job ceiling and still writes a report. When the pool will not shut down, the sweep kills what the workers spawned -- descendants, not just direct children, since flashers run in their own session -- confirms each kill actually landed, and exits early so the runner is freed. Whatever survived is named in the report. Deliberately shallow past that point. We do not re-scan process groups, prove pid ownership, or escalate through sudo: a root-owned survivor is reported, not force-killed, because signalling a pid we cannot prove is ours is the worse failure, and the job ceiling backstops whatever this misses. A D-state holder was never killable anyway. Recover instead of reporting a wedge. A HUNG usbtest case reflashes its own DUT through its roster flasher, but only where the flasher can reach its probe past a poisoned node -- openocd pinned to a validated vid_pid, or esptool. Where it cannot, the run says so rather than reserving budget for a path that cannot fire. Raise the CI ceilings above the pool guard so the guard fires first and still writes its report, and pin --retry 1 on every HIL leg: the guard is a flat constant and does not scale with max_retry, so argparse's default of 3 would triple the serialized usbtest tail against an unchanged guard. Split the module: execution in hil_test/hil_flash/usbtest, infrastructure in helper/ (locking, health, selection, shared bounded IO), and the two matrix generators into .github/scripts/ -- ci_set_matrix.py sat in workflows/, where GitHub treats every file as a workflow definition. 193 tests cover the bounded paths, the kill ladder, the guard and the selector against synthetic /proc trees and PATH-injected fakes; a real wedge cannot be manufactured on demand.
2026-07-30hil, ci: scope HIL builds and tests to the boards a PR affects (#3797)Ha Thach
hil, ci: scope HIL builds and tests to the boards a PR affects Add test/hil/hil_select.py, a stdlib-only selector that maps a PR diff to the rig boards, tests and BSP families a change can affect, and wire it into CI so pull requests build and run only those. A port change picks its families' boards, a class change picks the examples enabling that class, and device/host changes prune the other role. Anything unclassified — infra, an unmapped port, a selector error — falls back to the full matrix, and push/schedule runs are untouched. Move the shared example lists to hil_examples.py; 54 hardware-free tests cover the rules.
2026-07-29hil: split hil_test.py into hil_lock/hil_flash, add pool_check, update rig ↵Ha Thach
probes (#3794) test/hil: add board-pool health check, split hil_test into focused modules (#3794) Add test/hil/hil_pool_check.py: per-board rig health scan — probe presence, light-example flash (dfu_runtime; device_info + serial check for host-only boards), uid re-enumeration, safe recovery (probe authorized-toggle, board reset), verified board_test re-park, USB topology report, and a markdown summary table. Missing firmware is built on the spot (tools/build.py, idf.py for espressif, one get_deps retry); row statuses: ok, flash-failed, failed, locked. Board locks are always respected, never bypassed. Refactor hil_test.py into hil_lock.py (flock protocol, controller permits, hold/release/status CLI; replaces board_lock.py) and hil_flash.py (flashers, find_firmware, run_cmd). Update WCH probe uids and the board roster in tinyusb.json; add the hil-pool-check skill.
2026-07-28docs: add the unified OpenOCD plan, note worktree dep symlinkshathach
The plan doc records why the fork exists and how each vendor source was ported; the interim handoff it superseded is dropped. CLAUDE.md: a new worktree should symlink the dependency dirs to the primary checkout rather than re-fetching them, replacing a single symlink only when the branch needs a different dep revision. Also allow 'linke' in codespell - WCH-LinkE is a product name.
2026-07-24docs: plan for etm-trace tightening and target-debugger integrationhathach
2026-07-24Merge pull request #3786 from hathach/claude/improve-debug-skill-agentHa Thach
docs(skills): debug-skill overhaul — role-neutral capture model, verified debugger arsenal, Espressif backend
2026-07-24docs: address Codex/Copilot review on #3786hathach
- target-debug: fault frame lives on PSP when EXC_RETURN bit2 set (FreeRTOS tasks) — decode LR before choosing $msp/$psp (Codex; valid, our verify happened to fault on MSP) - esp-target-debug: show the Xtensa gdb for S3 in the attach recipe; clarify adapter serial = USB-SJ iSerial (colon MAC, hardware-verified) vs the CP2102N flasher uids in tinyusb.json (Codex; second half of the finding had the identifiers inverted) - esp plan: align serial form with the verified command; record the real console-gate outcome (UART0, USB-SJ console untested) (Copilot) - target-debug plan: Task 4 now consistently $JB/ARMv7-M matching the executed JLinkExe path (Copilot) - drop IDE-local .idea files swept in by the rename commit (Copilot)
2026-07-24docs(superpowers): esp-target-debug design spec + executed implementation planhathach
Spec (brainstormed): own-skill backend decision, PHY-conflict map, six verification gates, external-JTAG TODO. Plan executed same-day: all gates run on the rig; apptrace resolved per its own gate rule as (untested).
2026-07-24docs(target-debug): vector catch, SWO trace, verifybin, FreeRTOS threads; ↵hathach
table integration - Vector catch + Cortex-M fault autopsy, verified with a deliberate bad-load on stm32f407disco: CFSR=0x8200 (BFARVALID|PRECISERR), BFAR = exact bad address, stacked pc addr2lined to the faulting line; gotchas recorded (stale FPB comparators fire phantom SIGTRAPs — scrub first; arm DEMCR after reset; loads precise / stores imprecise; ARMv6-M has no CFSR/BFAR) - SWO exception trace + hw PC sampling gate PASSED on F407: 680 KB of packets in 3 s (0x17 PC samples in flash range, 0x0E SysTick enter/exit); JLinkSWOViewerCL decodes stimulus only — raw SWORead is the recipe; SWOStart needs an explicit speed headless - verifybin 'Verify successful.'; FreeRTOS -rtos plugin lists all 6 cdc_msc_freertos tasks after a run->stop cycle (plain attach = 0xDEAD placeholder); semihosting anti-note; monitor-mode pointer (untested) - Intrusiveness table gains the new rows; agent playbook bullet updated; retrieval gate 5/5 with a fresh reader; executed plan committed
2026-07-21Update docsJie Feng
2026-07-19misc fixesJie Feng
2026-07-18docs, udev: address Copilot review nits on PR #3775hathach
- getting_started.rst: the reST inline-link markup rendered literally inside the code-block (not a runnable command) and lacked sudo — use a plain `sudo cp examples/device/99-tinyusb-examples.rules ...`. - tools/88-tinyusb.rules: normalize the six MODE="666" entries to the 4-digit octal MODE="0666" used everywhere else in the file.
2026-07-17skill: add usb-sniffer — wire-level capture with the ataradov hardware taphathach
Fourth view in the USB debugging toolset (usbmon = host URBs, usb-debug = host reasoning, usb-target-debug = device firmware, usb-sniffer = what actually crossed D+/D-). Covers the ataradov/usb-sniffer analyzer: headless pcapng capture (--speed ls/fs/hs, --fold, --limit self-exit), Wireshark/tshark analysis recipes, and the wire realities that bite: downstream broadcast, sniffer self-capture noise, xHCI devnum != wire address, tap-point-dependent reset visibility (hub choreography anchors), FS-behind-HS-hub splits. Every recipe hardware-validated on the rig, including the capture-window floor (a 3 s window provably misses the enumeration ladder; 3M packets minimum). Two udev files with distinct audiences, not one: - examples/device/99-tinyusb-examples.rules (renamed from 99-tinyusb.rules): the user-facing rules the examples need — cafe VID access, hidraw, the ModemManager blacklist, a couple of board probes. getting_started.rst, the webusb_serial README and its source comment point here. - tools/88-tinyusb.rules: the HIL rig's private probe/analyzer allowlist, now with the sniffer (6666:6620 + blank FX2LP 04b4:8613). Installed on the rig only; the usb-sniffer skill references it.
2026-07-17agent: add target-debugger — device-side root-cause loop on the HIL righathach
Opus-tier agent charter for backgrounding a long hardware debug session: instrument -> build -> flash under one held board lock -> dual-side capture -> correlate -> refine, strictly one instance, skills as source of truth (usb-target-debug, usbmon, usb-debug, usb-sniffer, usb-recover, hil). The charter encodes what dogfooding established: - diagnosis standard: evidence must show the mechanism, or a fix must flip the ORIGINAL failing case on hardware; stop after two evidence-free cycles and hand back a partial diagnosis - lock cadence: hold for the whole session, release around hil_test.py runs (it self-locks per board) - revert semantics: "fix stays, probe goes, re-verify clean" — instrumentation reverted, candidate fix left uncommitted and re-verified on a clean build, pristine firmware reflashed before lock release Returns a machine-parseable diagnosis report including ruledOut[] — disproven hypotheses are deliverables. Spec roster updated (opus/xhigh, effort requested per agent() call).
2026-07-17skill: add usb-target-debug — device-side capture & debug on the HIL righathach
Completes the debugging toolset (usbmon = what the host exchanged, usb-debug = why the host acted, usb-sniffer = what crossed the wire): TU_LOG/RTT capture, per-probe GDB autopsy without reset, RAM ring-buffer event trace, J-Link DWT_PCSR PC-sampling, dual-side capture posture, and board-lock rig discipline. Includes the implementation plan it executes. Hard-won warnings baked in from real bring-up sessions: volatile ring buffers vs -Os dead-store elimination, RTT NO_BLOCK_SKIP post-mortem limits (no overwrite mode exists), DHCSR validity anchors for register snapshots, release-lock-before-hil_test, and that a marginal just-recabled link can fake a deterministic firmware bug. Also ignore .claude/worktrees/.
2026-07-13Fix max-effort review findings in lock protocol, workflows, and docshathach
Confirmed by a 10-finder / 28-verifier adversarial review pass: board_lock.py — the flock is now the sole authority: drop cmd_hold's pid-liveness pre-gate (a live hil_test.py pool worker's stale record no longer blocks a genuinely free board); cmd_release probes the flock and only signals a verified holder, refuses to kill hil_test.py holders (CI mid-test), handles PermissionError; the holder daemon truncates its lock records on SIGTERM and keeps the success pipe clear of fds 0-2 (closed-stdio hold used to leave an orphan holder while reporting failure); --config default resolves beside the script. hil_test.py — truncate the lock record on per-board release (pool workers outlive their flocks); warn instead of silently failing open when the lock dir is unusable; error out on -b names absent from the config (was a silent zero-test exit 0, readable as a green HIL run); drop an emptied board row in accumulate_report (variant boards left a blank ghost row). workflows — remove the stray positional arg that made the validate size stage exit 2 on every run; wrap JSON.parse(args) in all six scripts; factor pr-babysit's drifted reply recipe into postReplyRecipe and dedup refutation replies across cycles; validate args.pr and maxCycles; driver-review rejects an empty dimensions list; hil-validate drops a dead guard clause and retries diagnostics with -v -r 1. agents/docs — port-dev scopes git clang-format to its own files (concurrent workers reformatted each other in shared checkouts); hil-operator/hil skill wording matches actual fail-fast output; the implementation plan is now a DO-NOT-EXECUTE historical record (banner + checked boxes) so plan-executing agents cannot revert shipped files. Verified: lock storm 1-winner-in-10, stale-record hold, closed-stdio hold, dead-pid cleanup, CI-holder refusal, ghost-row 4-scenario merge, unknown-board exit 1, py_compile + check.sh on all six workflows, pre-commit clean. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Upj4hta5TNoAbidqeC1zZ6
2026-07-10Fix review findings and add static-analyzer agenthathach
Review-fix batch (owner-confirmed) on the multi-agent harness: - board_lock: detach holder stdio so a captured `hold` cannot hang on the daemon's inherited pipe; probe locks by holder-pid liveness instead of a momentary flock, which could spuriously fail a concurrent acquirer (storm-tested: 1 winner in 10, 0/15 acquire failures under probe storm) - hil_test: locked board renders a visible board-locked fail row so the report matches the exit code; stale marker cleared on a real re-run - pr-babysit: autoPush now opt-in (default dry run); resolve recipe paginates reviewThreads; post-push resolve gets issue-comment fallback - validate: size stage honors non-default base via --base-branch; pvs stage delegated to the new agent - new static-analyzer agent (sonnet): PVS-Studio SAST+MISRA for one board, structured findings gated on files changed vs base Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Rn1AN5DsTdFhRwhugfgKZi
2026-07-09feat: Claude Code multi-agent dev/test harness for TinyUSBhathach
Add worker agents (builder, port-dev, driver-reviewer, hil-operator, pr-monitor), deterministic workflows (validate, fanout-dev, driver-review, hil-validate, full-check, pr-babysit) and a /pre-pr gate skill, so sessions can fan build/test/review/PR-triage work out to tiered subagents. pr-babysit drives a PR to green: triage CI + bot reviews, fix validated findings, verify, push, and reply-to + resolve each inline review thread (fixed or refuted). Replace the stop-the-runner HIL discipline with per-board flock locks: test/hil/board_lock.py plus a fail-open guard in hil_test.py let CI and dev sessions share the rig per board (locked boards fail fast and re-run; HIL_NO_BOARD_LOCK=1 is a user-authorized bypass). The actions-runner is never stopped. Design spec, implementation plan, and real-rig smoke evidence under docs/superpowers/. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Rn1AN5DsTdFhRwhugfgKZi