<feed xmlns='http://www.w3.org/2005/Atom'>
<title>tinyusb.git/docs/superpowers/specs, branch hil-report</title>
<subtitle>Unnamed repository; edit this file 'description' to name the repository.</subtitle>
<id>http://cgit.235523.xyz/tinyusb.git/atom/docs/superpowers/specs?h=hil-report</id>
<link rel='self' href='http://cgit.235523.xyz/tinyusb.git/atom/docs/superpowers/specs?h=hil-report'/>
<link rel='alternate' type='text/html' href='http://cgit.235523.xyz/tinyusb.git/'/>
<updated>2026-08-21T15:25:13Z</updated>
<entry>
<title>docs: design, plan and follow-ups for the hil_report consolidation</title>
<updated>2026-08-21T15:25:13Z</updated>
<author>
<name>hathach</name>
<email>thach@tinyusb.org</email>
</author>
<published>2026-08-21T15:16:25Z</published>
<link rel='alternate' type='text/html' href='http://cgit.235523.xyz/tinyusb.git/commit/?id=aa4a07820dd2dd54fa5fef0375c709d67b1c78d0'/>
<id>urn:sha1:aa4a07820dd2dd54fa5fef0375c709d67b1c78d0</id>
<content type='text'>
Records why the report code was spread across three modules and what the
consolidation buys, so the next reader does not have to re-derive it from the
diff. Names the new functions explicitly rather than presenting the change as
pure code motion -- that framing points reviewers away from the code that
carried the defects.

Three findings this PR deliberately does not close get one handoff each, per
CLAUDE.md: the worker-result tuple hil_report still unpacks positionally,
SKILL.md's no-boards rule drifting from the code, and write_report's two
non-atomic writes. Drops the pr3836 handoff, which this branch implements.
</content>
</entry>
<entry>
<title>ci: fix nine ways the selection under-selected or mismatched</title>
<updated>2026-08-21T05:41:47Z</updated>
<author>
<name>hathach</name>
<email>thach@tinyusb.org</email>
</author>
<published>2026-08-21T05:41:47Z</published>
<link rel='alternate' type='text/html' href='http://cgit.235523.xyz/tinyusb.git/commit/?id=e13eff8d4e757ebe7709a58fce44017b8be5a84d'/>
<id>urn:sha1:e13eff8d4e757ebe7709a58fce44017b8be5a84d</id>
<content type='text'>
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 -&gt; 0.29s there, 0.8s -&gt; 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.
</content>
</entry>
<entry>
<title>docs: record the CI selection design and its plan</title>
<updated>2026-08-21T04:09:03Z</updated>
<author>
<name>hathach</name>
<email>thach@tinyusb.org</email>
</author>
<published>2026-08-21T04:09:03Z</published>
<link rel='alternate' type='text/html' href='http://cgit.235523.xyz/tinyusb.git/commit/?id=f96ddbaa1e11a98f8076df48ba73026191c28399'/>
<id>urn:sha1:f96ddbaa1e11a98f8076df48ba73026191c28399</id>
<content type='text'>
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.
</content>
</entry>
<entry>
<title>docs: spec, plan and outcome of the .claude instruction-surface audit</title>
<updated>2026-08-20T09:47:23Z</updated>
<author>
<name>hathach</name>
<email>thach@tinyusb.org</email>
</author>
<published>2026-08-20T09:47:23Z</published>
<link rel='alternate' type='text/html' href='http://cgit.235523.xyz/tinyusb.git/commit/?id=c514a6ee91a53927f55ba35e0a2933eddb5cefeb'/>
<id>urn:sha1:c514a6ee91a53927f55ba35e0a2933eddb5cefeb</id>
<content type='text'>
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.
</content>
</entry>
<entry>
<title>examples: document and work around the i.MX RT and LPC55 USB errata</title>
<updated>2026-08-18T15:07:49Z</updated>
<author>
<name>hathach</name>
<email>thach@tinyusb.org</email>
</author>
<published>2026-08-16T18:02:54Z</published>
<link rel='alternate' type='text/html' href='http://cgit.235523.xyz/tinyusb.git/commit/?id=19ff2ed615e4a97984aab5551ac8835ead53b9e7'/>
<id>urn:sha1:19ff2ed615e4a97984aab5551ac8835ead53b9e7</id>
<content type='text'>
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.
</content>
</entry>
<entry>
<title>skills, docs: rewrite USB recovery from the live incidents</title>
<updated>2026-08-18T05:19:09Z</updated>
<author>
<name>hathach</name>
<email>thach@tinyusb.org</email>
</author>
<published>2026-08-13T18:08:50Z</published>
<link rel='alternate' type='text/html' href='http://cgit.235523.xyz/tinyusb.git/commit/?id=f822f69a9871b2115c5213889d70411da66ca1b1'/>
<id>urn:sha1:f822f69a9871b2115c5213889d70411da66ca1b1</id>
<content type='text'>
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.
</content>
</entry>
<entry>
<title>test/hil, ci: contain a wedged USB stack instead of stranding the runner</title>
<updated>2026-08-18T05:19:09Z</updated>
<author>
<name>hathach</name>
<email>thach@tinyusb.org</email>
</author>
<published>2026-08-13T18:08:40Z</published>
<link rel='alternate' type='text/html' href='http://cgit.235523.xyz/tinyusb.git/commit/?id=3963a1b70a572132aced1c1a0033e1c8249a0c7e'/>
<id>urn:sha1:3963a1b70a572132aced1c1a0033e1c8249a0c7e</id>
<content type='text'>
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.
</content>
</entry>
<entry>
<title>hil, ci: scope HIL builds and tests to the boards a PR affects (#3797)</title>
<updated>2026-07-29T19:29:32Z</updated>
<author>
<name>Ha Thach</name>
<email>thach@tinyusb.org</email>
</author>
<published>2026-07-29T19:29:32Z</published>
<link rel='alternate' type='text/html' href='http://cgit.235523.xyz/tinyusb.git/commit/?id=eef5af86aa26fe3d72e41156a586a6ed3ffce9f8'/>
<id>urn:sha1:eef5af86aa26fe3d72e41156a586a6ed3ffce9f8</id>
<content type='text'>
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.</content>
</entry>
<entry>
<title>hil: split hil_test.py into hil_lock/hil_flash, add pool_check, update rig probes (#3794)</title>
<updated>2026-07-29T10:29:59Z</updated>
<author>
<name>Ha Thach</name>
<email>thach@tinyusb.org</email>
</author>
<published>2026-07-29T10:29:59Z</published>
<link rel='alternate' type='text/html' href='http://cgit.235523.xyz/tinyusb.git/commit/?id=e88fc441ddcaaa5abd4f4673ef2bf29499522dc0'/>
<id>urn:sha1:e88fc441ddcaaa5abd4f4673ef2bf29499522dc0</id>
<content type='text'>
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.</content>
</entry>
<entry>
<title>docs(superpowers): esp-target-debug design spec + executed implementation plan</title>
<updated>2026-07-24T07:55:59Z</updated>
<author>
<name>hathach</name>
<email>thach@tinyusb.org</email>
</author>
<published>2026-07-24T07:55:59Z</published>
<link rel='alternate' type='text/html' href='http://cgit.235523.xyz/tinyusb.git/commit/?id=df3cea3d0e12c155e2a2e7f81dec031da38ab328'/>
<id>urn:sha1:df3cea3d0e12c155e2a2e7f81dec031da38ab328</id>
<content type='text'>
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).
</content>
</entry>
</feed>
