Skip to content

feat(port): fbuild port doctor — diagnose port presence/health/power policy, with a safe --fix #1279

Closed

Description

@zackees

Revised. This issue originally proposed fbuild port suspend as a standalone command. After surveying prior art it is re-scoped around a diagnosis-first fbuild 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:

  1. cycle is the verb people actually want — uhubctl makes it first-class. Prevention is nice; recovery is what unblocks a bench.
  2. Prevention and recovery are separate concerns. powercfg/registry is policy; uhubctl is recovery. One verb covering both would be a design error.
  3. Windows cannot do a true VBUS cycle in software at all. Hard constraint, not a tooling gap.
  4. 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-run prints 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 / --plan require --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 doctor is 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 doctor is 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.
  • --fix round-trips: disable-then-revert restores the original per-device state, including nodes already disabled beforehand.
  • Exactly one UAC prompt per --fix; --no-elevate fails cleanly; --dry-run neither elevates nor writes.
  • Re-running --fix is a no-op.
  • Non-Windows exits 0 with "not applicable" plus the platform-appropriate equivalents.
  • fbuild port scan shows present= and suspend= per port, with a footer pointing at doctor.

Related

Activity

  1. changed the title [-]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[/+] on Aug 7, 2026
  2. zackees commented on Aug 7, 2026

    @zackees
    MemberAuthor

    Progress: the diagnosis half landed in #1280 (eef651e7) — present=no in scan, fbuild port doctor, and suspend-policy reporting. The remedy half (--fix) is open as #1281.

    Still open against this issue's acceptance criteria: the scan footer + per-port suspend=, per-device (registry) suspend state — without which --fix cannot round-trip prior per-device state — --json, --all/--hub selectors, and last-seen=.

    Full context, dead ends and local/CI parity notes for whoever picks this up: #1282.

  3. zackees commented on Aug 7, 2026

    @zackees
    MemberAuthor

    Done across four PRs, all merged:

    PR
    #1280 (eef651e7) fbuild port doctor; present=no in scan
    #1281 (a0f4b91f) doctor --fix
    #1283 (0132ce40) scan footer; doctor --json
    #1284 (8d2a6dbc) per-device suspend; last-seen; --all/--hub

    The command against the board that motivated this:

    $ fbuild port doctor --port COM17
    COM17
      presence   NOT PRESENT
      last seen  5d ago
      health     phantom
      topology   parent USB\VID_05E3&PID_0610\7&3AFC677D&0&1
      suspend    this port's hub chain may be powered down by Windows
      verdict    not attached — this is a stale registry record, not a fault
      remedy     plug the board in, then re-run `fbuild port scan`
    
    unassociated USB problem device(s)
      Unknown USB device
        problem 43 at Port_#0014.Hub_#0001  instance USB\VID_0000&PID_0002\...
      NOTE: these are not attributed to any port above.
    

    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

    • read-only, never elevates or prompts
    • reports presence and last-arrival, and says plainly when a port is a stale record rather than a fault
    • never attributes an unassociated problem device to a port — own section, explicit warning
    • reports suspend at both the plan and per-device layers
    • --fix guards: one UAC prompt, --no-elevate fails cleanly, --dry-run neither elevates nor writes, re-running is a no-op
    • non-Windows exits cleanly with "not applicable"
    • scan shows present= with a footer pointing at doctor
    • --json, --all, --hub

    Two deviations, both deliberate

    scan does not show suspend= per port. It needs a WMI call and scan is a hot path used by deploy; ~0.5 s on every scan to surface a diagnostic is the wrong trade. It lives in doctor.

    last-seen is 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, --fix without --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 --yes before 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=False left a probe returning nothing while its tests passed
    • a suspend lookup matching one hop returned None for every real port
    • --json, --all, --hub, --port were accepted and ignored in fix mode
    • an unrecognised Enable value silently became false — violating the "unknown must not become false" rule applied three times elsewhere in the same file

    Worth knowing for anyone extending this: mocked tests did not catch any of them. Running the command did.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions