Repository navigation
Description
Revised. This issue originally proposed
fbuild port suspendas a standalone command. After surveying prior art it is re-scoped around a diagnosis-firstfbuild port doctor, with the suspend controls folded in as its remedy. Rationale below.
Problem
fbuild port scan cannot distinguish "this board is unplugged" from "this board is broken." Both render as health=phantom selectable=no.
That ambiguity cost a full investigation in FastLED/FastLED#3864. The board was reported as a wedged devnode needing BOOTSEL and scoped PnP recovery. Windows actually said:
Get-PnpDevice -Class Ports -> COM17 Present = False
DEVPKEY_Device_LastArrivalDate -> 2026-08-01 22:27:59
DEVPKEY_Device_LastRemovalDate -> 2026-08-01 22:41:50
It had simply not been plugged in for six days. Worse, a different board — a genuinely descriptor-failed one on a different root port — was present with code 43, and conflating the two is what produced the "wedged" reading. Hours went into recovery paths for a device that was not there.
A single command printing Present=False, last seen 6 days ago would have ended it immediately.
Prior art surveyed
| Tool | Mechanism | Windows |
|---|---|---|
| uhubctl | Per-port VBUS switching on smart hubs; -a off/on/cycle/toggle, -p 2-5, -l b-x.y.z |
No — libusb/winusb.sys cannot issue the required control requests |
powercfg |
Global power-plan selective suspend (SUB_USB / 48e6b7a6-...) |
Yes |
| Windows registry | Per-device DeviceIdleEnabled / DefaultIdleState / DefaultIdleTimeout / UserSetDeviceIdleEnabled under HKLM\SYSTEM\CurrentControlSet\Enum\<dev>\<inst>\Device Parameters; global Services\USB\DisableSelectiveSuspend |
Yes — and more durable than the WMI MSPower_DeviceEnable view |
| Linux | /sys/bus/usb/devices/*/power/control, udev ATTR{power/autosuspend}="-1", usbcore.autosuspend=-1 |
n/a |
| YKUSH / Acroname | Hardware switchable hubs, HID-based, ship a Windows CLI (ykushcmd) |
Yes |
| pykiso | Test framework wrapping YKUSH for exactly this | — |
Lessons taken:
cycleis the verb people actually want — uhubctl makes it first-class. Prevention is nice; recovery is what unblocks a bench.- Prevention and recovery are separate concerns. powercfg/registry is policy; uhubctl is recovery. One verb covering both would be a design error.
- Windows cannot do a true VBUS cycle in software at all. Hard constraint, not a tooling gap.
- Per-device suspend on Windows is properly a registry property; the WMI checkbox is the shallow view.
Proposal: fbuild port doctor
fbuild port doctor [--port COM17 | --all] [default: --all]
[--fix] [--dry-run] [--yes] [--no-elevate] [--json]
Diagnosis-first, in the brew doctor / flutter doctor idiom. It answers, in one place, the questions that were expensive to answer separately.
What it reports, per port
- Presence —
Present,LastArrivalDate,LastRemovalDate. This is the headline; it is what was missing. - Health — fbuild's existing label, plus the Windows problem code and its plain-English meaning when set (e.g.
CM_PROB_FAILED_POST_START-> "device descriptor request failed"). - Topology — the USB ancestor chain, so a code-43 node on a different hub is never mistaken for this board.
- Policy — selective suspend at both layers (power plan and per-device), since disabling one does not disable the other.
- Recovery backends present — uhubctl / ykushcmd / Acroname / none.
- Verdict + remedy, explicitly split into fbuild can fix this vs this needs hands.
Sketch
$ fbuild port doctor --port COM17
COM17 2E8A:F00F Arduino / Generic RP2350 ser=2DCB876B587EA334
presence NOT PRESENT - last seen 2026-08-01 22:27:59, removed 22:41:50 (6d ago)
topology was on USB\VID_05E3&PID_0610\7&3afc677d&0&1 (hub present and healthy)
verdict the board is not attached; this is a stale registry record, not a fault
remedy plug the board in, then re-run `fbuild port scan`
note an unrelated code-43 device sits on Port_#0014.Hub_#0001 (a different
root port) - it is NOT this board
host
suspend power plan (Balanced): ENABLED (AC+DC); 18/18 USB nodes allow power-down
verdict Windows may power a port down mid-session; a board that does not resume
returns as code 43 and its COM record reads health=phantom
remedy fbuild port doctor --fix (elevates once)
recovery no VBUS-cycle backend found (uhubctl unsupported on Windows;
no ykushcmd/Acroname hub detected)
--fix
Applies only the safe, reversible subset — currently: disable selective suspend (per-device registry, and the plan setting with --plan).
--dry-runprints the exact change set and never elevates.- Idempotent: already-disabled is a no-op that neither prompts nor errors.
- Exactly one UAC prompt per invocation, batching the whole change set.
- Records prior per-device state so the change is reversible with fidelity — a node already disabled beforehand stays disabled on revert.
--all/--planrequire--yes.- Non-Windows: exit 0, "not applicable", after reporting Linux/macOS equivalents.
What --fix must NOT do
Deliberately excluded, because these look helpful and are not:
- No device disable/enable or root-hub restart. A bus-level reset is not a VBUS cycle and does not recover a descriptor-failed device. FastLED's own history has a board where this was tried repeatedly and never worked; restarting the root hub also disrupts every other device on it.
- No auto-elevation without an explicit action. Plain
doctoris read-only and must never prompt. - No "recovery" claims it cannot honour. When no VBUS backend exists, say so and stop.
port scan integration
Annotate the existing second row and add a footer, so this is discoverable without knowing the subcommand exists:
COM17 2E8A:F00F USB Serial Device (COM17) ser=2DCB876B587EA334
|- Arduino / Generic RP2350 health=phantom present=no last-seen=6d suspend=allowed
note: 1 port not present (stale record, not a fault); 3 port(s) may be powered down by Windows.
Run `fbuild port doctor` for details.
Distinguishing health=phantom from present=no in the output alone would have prevented the original misdiagnosis.
Deferred: fbuild port power {on,off,cycle} (uhubctl-shaped)
Worth doing, but not first. Adopting uhubctl's verb set is the right long-term ergonomics, and cycle is the genuinely valuable primitive. The blocker is that it cannot run where the problem is: uhubctl has no Windows support, and without a YKUSH/Acroname hub there is no software VBUS cycle on Windows at all. Shipping the headline verb into an environment that cannot execute it is worse than not shipping it.
Revisit when a switchable hub is on the bench; then fbuild port doctor gains a real recovery backend and --fix can legitimately offer a power cycle.
Hardware note: for unattended HIL on Windows, a YKUSH or Acroname hub is the actual unlock — it is the only way to get a true VBUS cycle. Roughly $50-100 converts "walk over and replug it" into an automated step.
Acceptance criteria
fbuild port doctoris read-only and never elevates or prompts.- It reports presence and last-arrival/removal, and states plainly when a port is a stale record rather than a fault.
- It never attributes an unassociated problem device to a specific board; unmatched code-43 nodes are reported separately with their location.
- It reports selective suspend at both the plan and per-device layers.
--fixround-trips: disable-then-revert restores the original per-device state, including nodes already disabled beforehand.- Exactly one UAC prompt per
--fix;--no-elevatefails cleanly;--dry-runneither elevates nor writes. - Re-running
--fixis a no-op. - Non-Windows exits 0 with "not applicable" plus the platform-appropriate equivalents.
fbuild port scanshowspresent=andsuspend=per port, with a footer pointing atdoctor.
Related
- chore(rp2350w): hand off final #3832 hardware validation FastLED#3864 — where the ambiguity cost real time
- feat(autoresearch): warn when USB selective suspend can drop a HIL board FastLED#3872 — FastLED-side selective-suspend warning this would supersede/feed
- fix(rp2040): reject phantom CDC ports and give recoverable BOOTSEL guidance #1147 — unhealthy CDC records excluded from deploy selection
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsTriage
Activity
[-]feat(port): `fbuild port suspend` — inspect and disable Windows USB selective suspend[/-][+]feat(port): fbuild port doctor — diagnose port presence/health/power policy, with a safe --fix[/+]feat(port): add `fbuild port doctor`
feat(port): report USB selective suspend in doctor
feat(port): add `doctor --fix` for USB selective suspend
zackees commentedon Aug 7, 2026
Progress: the diagnosis half landed in #1280 (
eef651e7) —present=noinscan,fbuild port doctor, and suspend-policy reporting. The remedy half (--fix) is open as #1281.Still open against this issue's acceptance criteria: the
scanfooter + per-portsuspend=, per-device (registry) suspend state — without which--fixcannot round-trip prior per-device state —--json,--all/--hubselectors, andlast-seen=.Full context, dead ends and local/CI parity notes for whoever picks this up: #1282.
feat(port): add `doctor --fix` for USB selective suspend (#1281)
feat(port): per-device suspend state in doctor
feat(port): report last-seen for a targeted port
feat(port): add --all and --hub scope selectors to doctor
feat(port): per-device suspend state in doctor
feat(port): report last-seen for a targeted port
feat(port): add --all and --hub scope selectors to doctor
feat(port): scan footer pointing at doctor, and doctor --json (#1283)
feat(port): per-device suspend state in doctor
feat(port): report last-seen for a targeted port
feat(port): add --all and --hub scope selectors to doctor
feat(port): per-device suspend, last-seen, and --all/--hub selectors (#…
zackees commentedon Aug 7, 2026
Done across four PRs, all merged:
eef651e7)fbuild port doctor;present=noinscana0f4b91f)doctor --fix0132ce40)doctor --json8d2a6dbc)last-seen;--all/--hubThe command against the board that motivated this:
Every line there answers something that cost time in FastLED/FastLED#3864: it was not attached, it had not been for days, and the code-43 device people kept pointing at is a different board on a different hub.
Acceptance criteria
--fixguards: one UAC prompt,--no-elevatefails cleanly,--dry-runneither elevates nor writes, re-running is a no-opscanshowspresent=with a footer pointing atdoctor--json,--all,--hubTwo deviations, both deliberate
scandoes not showsuspend=per port. It needs a WMI call andscanis a hot path used by deploy; ~0.5 s on every scan to surface a diagnostic is the wrong trade. It lives indoctor.last-seenis single-port only. Measured ~0.74 s per device with no batching — 12.5 s for all 17 ports on this bench, 0.7 s for one. It appears with--port, which is exactly when the question is asked.--fix's elevated execution path is not verified. Every guard is (--dry-run→ 0,--fixwithout--yes→ 1,--no-elevate→ 1), but the UAC prompt itself was never triggered — not something to fire from an unattended session. Worth one manual--fix --yesbefore relying on it.What review caught
Six real defects, three of them the same shape — a flag or value that silently does nothing:
capture_output=Falseleft a probe returning nothing while its tests passedNonefor every real port--json,--all,--hub,--portwere accepted and ignored in fix modeEnablevalue silently becamefalse— violating the "unknown must not become false" rule applied three times elsewhere in the same fileWorth knowing for anyone extending this: mocked tests did not catch any of them. Running the command did.