Skip to content

fix(serial): preserve Windows PnP health through port probing #1146

Description

@zackees

Outcome

Make port health a first-class part of fbuild's serial-device model. Windows already discovers whether a COM devnode is live, phantom, or in a Config Manager problem state; this issue stops that information from being discarded before device selection.

This is the foundation for #1147. It must land before RP2040 deployment tries to reject stale CDC endpoints.

Confirmed failure

fbuild_serial::ports::available_ports() currently inspects Windows PnP state and intentionally retains unusual/non-present USB COM records for diagnostic compatibility with #962. It then returns only serialport::SerialPortInfo.

That return type loses:

  • whether the devnode is present in the live tree;
  • Config Manager status and problem code;
  • canonical device-instance ID;
  • parent/topology identity already discovered during the PnP walk.

A stale RP2040 entry can therefore look identical to a live COM12 later in deployment. This is a data-model bug, not an RP2040 timeout bug.

Required data model

Add a public record in crates/fbuild-serial/src/ports.rs (names may vary, semantics may not):

struct DetectedPort {
    info: serialport::SerialPortInfo,
    health: PortHealth,
    instance_id: Option<String>,
    parent_instance_id: Option<String>,
    // Reuse existing topology/location data when available.
}

enum PortHealth {
    HealthyPresent,
    PresentProblem { problem_code: u32, status: Option<u32> },
    Phantom { problem_code: Option<u32>, status: Option<u32> },
    Unknown,
}

Classification rules:

  • HealthyPresent: Windows says the devnode is present and its problem code is zero.
  • PresentProblem: present is true and the problem code is non-zero.
  • Phantom: present is false, regardless of whether Windows also reports a problem code.
  • Unknown: the host does not expose equivalent metadata (normal on non-Windows), the endpoint is non-USB, or the Windows metadata query failed.
  • Preserve the canonical Windows instance ID exactly as returned by PnP. Display escaping is a rendering concern; matching must use the canonical value.
  • Add helpers such as is_known_unhealthy(). Unknown is not the same as unhealthy; existing non-Windows behavior must continue and consumers can use an openability probe.

Implementation directions

  1. Refactor crates/fbuild-serial/src/ports.rs so the blessed enumeration API returns DetectedPort, not a lossy SerialPortInfo.
  2. Keep phantom/problem records in the returned list. Diagnostic visibility from fbuild port scan misses all PJRC/Teensy (VID 16C0) serial ports + no vendor/product resolution for them #962 is required; filtering them out at enumeration time would recreate that regression.
  3. Do not derive health by joining only against present_usb_problem_devices(): that helper uses a present-only Windows scan and cannot represent a phantom devnode. Build the health record during the same PnP walk that currently computes present.
  4. Reuse the existing Windows USB topology/PnP helpers added for feat(rp2040): warn on host-detected failed USB devices and hub attachment #1087. Do not add a second independent SetupAPI traversal if the existing record can be enriched.
  5. If a lossy compatibility adapter is temporarily unavoidable, name it explicitly (for example available_port_infos_lossy()) and do not use it in deploy, monitor, daemon inventory, or auto-selection.
  6. Migrate crates/fbuild-cli/src/cli/port_scan.rs to render health, problem code, and instance ID. A phantom endpoint should be visible and clearly marked as not selectable.
  7. Migrate the daemon device inventory so cached device records retain health instead of caching only port name/VID/PID.
  8. Audit every caller with:
    rg -n "available_ports\(" crates
    
    At minimum classify the direct calls in CLI serial probing, daemon device management, fbuild-serial board/bootloader watching, Teensy discovery, RP2040 discovery, and platform deployers. Migrate any caller that chooses a user device. Tool-specific probes may remain raw only when the PR explains why health metadata is irrelevant.
  9. Keep selection policy out of the low-level enumerator. The enumerator reports facts; family deployers decide how known-unhealthy devices affect their recovery state machine.

Required diagnostics

A Windows port-scan row must distinguish at least:

  • healthy and present;
  • present with problem code;
  • phantom/non-present;
  • unknown health.

When known, include the canonical instance ID and Config Manager problem code. Do not report a phantom record as merely ?available.?

Tests

Tests must not depend on the contributor's real Device Manager state. Extract classification/merge logic behind testable functions and use fixtures for:

Run:

soldr cargo test -p fbuild-serial
soldr cargo test -p fbuild-cli port_scan
soldr cargo test -p fbuild-daemon device
soldr cargo clippy -p fbuild-serial -p fbuild-cli -p fbuild-daemon --all-targets -- -D warnings

If a name filter matches no tests, run the whole package and record that command instead.

Acceptance criteria

  • The blessed fbuild enumeration result carries health, presence, problem code/status, and canonical instance ID where Windows exposes them.
  • Health survives CLI scan, daemon inventory/cache, and handoff to deploy selection.
  • Diagnostic scans still show the unusual composite endpoints retained for fbuild port scan misses all PJRC/Teensy (VID 16C0) serial ports + no vendor/product resolution for them #962.
  • A known phantom/problem endpoint can be distinguished from a healthy endpoint without trying to open it.
  • Non-Windows behavior remains operational through Unknown plus existing openability checks.
  • The PR contains a caller inventory from rg -n "available_ports\(" crates and explains every selection path left on raw/lossy enumeration.
  • Unit tests cover all states above and pass without attached hardware.

Non-goals

Related issues

Implementation handoff: exact migration boundary

This child is a model-and-propagation change, not a selection-policy change. Keep DetectedPort public and value-like (Clone, Debug, equality where practical), with public info, health, instance_id, and parent_instance_id fields or equivalent accessors. On non-Windows, wrap the existing serialport result as Unknown; do not return raw SerialPortInfo from the blessed API on one platform and enriched records on another.

Implement one pure classification seam (for example PnpObservation -> PortHealth) and test it without Windows hardware. CM_Get_DevNode_Status success plus problem 0 is healthy; successful status with non-zero problem is PresentProblem; a non-present/phantom observation is Phantom; failure or unavailable metadata is Unknown. Keep the canonical instance ID unmodified for matching. A UI renderer may escape it, but must not write the escaped form back into the record.

Caller decision table

Migrate these callers to the enriched API in this PR because they list, cache, monitor, match, or select user devices:

Caller Required treatment
fbuild-cli/src/cli/port_scan.rs render health, problem code, and instance ID; annotate known-unhealthy rows as selectable=no without hiding them
fbuild-cli/src/cli/deploy.rs and serial_probe.rs retain health through monitor/list/find decisions; never implicitly select a known-unhealthy endpoint
fbuild-daemon/src/device_manager.rs retain health and identities in discovered and cached device state
fbuild-serial/src/bootloader_watcher.rs and boards.rs consume enriched records; exclude only at the policy point if opening/selecting a known-unhealthy port
fbuild-deploy/src/rp2040.rs and teensy/port_discovery.rs carry enriched facts forward; RP2040's actual rejection/recovery behavior remains #1147

Audit remaining direct serialport::available_ports() calls individually. probe_rs, LPC, ESP32-native, or similarly tool-specific probes may stay raw only if the PR lists the exact function and explains why it does not select a CDC endpoint or cannot use PnP health. Never silently leave a direct user-device selection path raw.

Do-not-merge checks

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    Status
    Triage

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions