Skip to content

fix(rp2040): consume Windows port health in deploy selection and reacquisition - #1153

Merged
zackees merged 11 commits into
mainfrom
fix/1147-rp2040-health-gate
Jul 23, 2026
Merged

fix(rp2040): consume Windows port health in deploy selection and reacquisition#1153
zackees merged 11 commits into
mainfrom
fix/1147-rp2040-health-gate

Conversation

@zackees

@zackees zackees commented Jul 23, 2026

Copy link
Copy Markdown
Member

Closes #1147 (tracked by meta #1149).

What changed

RP2040 deployment never treats a stale Windows CDC history record as a usable runtime port. Health from #1146 (PR #1150) now gates every step of the state machine, and the daemon no longer re-blesses the requested pre-flash COM name.

Files / state transitions

  • crates/fbuild-deploy/src/rp2040_target.rs
    • select_cdc_candidate filters known-unhealthy (Phantom / PresentProblem) records before serial/previous-port/new-port matching and returns the full health-carrying PicoCdcPort instead of a bare name. Unknown health (non-Windows) stays eligible.
    • resolve_requested_runtime_target fails an explicit --port COMx / SER= selector that matches a known-unhealthy record, printing health label, problem code, and instance ID instead of touching the stale handle (no 1200-baud touch on it).
  • crates/fbuild-deploy/src/rp2040.rs
    • wait_for_cdc_port_with_clock gains an injectable bounded openability probe: a selected candidate that fails open() is never returned; the loop keeps polling and the last open error lands in the timeout diagnostics.
    • CdcWaitTimeout carries last_open_error and emits phantom-aware recovery guidance (stale devnode identity + exact BOOT/RESET → RPI-RP2 steps + direct-port/cable advice).
    • resolve_post_flash_cdc confirmed-flash timeout note now includes the full diagnostics and guidance — flash success and CDC recovery are reported separately (port: None).
    • Rp2040Deployer::owns_post_flash_port_discovery() == true.
  • crates/fbuild-deploy/src/lib.rs — new default-false Deployer::owns_post_flash_port_discovery(); legacy deployers are unchanged.
  • crates/fbuild-daemon/src/handlers/operations/deploy.rs
    • The dangerous anchor deploy_port_str.or_else(|| deploy_result.port) is replaced by resolve_recovery_port(...): when the deployer owns port discovery, only the freshly catalogued, health-eligible, open-verified DeploymentResult.port is used for clear_preemption / post_deploy_recovery / monitor attach; None means nothing is touched or monitored (monitor was skipped response instead of /dev/ttyUSB0 fallback).
    • FBUILD_DEPLOY_PORT= stays empty (never stale) when no port was recovered, and the deployer's recovery diagnostic — previously discarded on success — is appended to the response message.
  • crates/fbuild-daemon/src/handlers/operations/deploy_port.rs
    • choose_deploy_port (RaspberryPi arm) partitions known-unhealthy device records out of auto-selection via partition_rp_candidates + rp_deploy_choice; they are reported in a warning (health/problem/instance, fbuild port scan pointer) and deploy continues on the BOOTSEL-volume path.

Deterministic tests added

rp2040_target.rs: phantom serial match never selected; present-problem previous port never reselected; healthy record wins over phantom history with the same serial; unhealthy new port creates no ambiguity; explicit selector on phantom fails with health details; Unknown health stays eligible.
rp2040.rs: open-probe failure never returned + lands in diagnostics; candidate that stays phantom is never returned (probe unreachable); confirmed flash + phantom history yields instance/health/problem/last-open-error/BOOTSEL guidance; existing #1095 delayed-handoff and renumbering tests updated for the probe seam.
deploy.rs (daemon): owned port discovery never substitutes the requested pre-flash port; legacy deployers keep requested-first order.
deploy_port.rs (daemon): phantom RP2040 CDC never auto-selected (with diagnostic warning); healthy selected while phantom is reported; present-problem excluded; unknown health eligible.

Validation

soldr cargo test -p fbuild-deploy        -> 241 passed, 0 failed
soldr cargo test -p fbuild-daemon --lib  -> 203 passed, 0 failed
soldr cargo test -p fbuild-cli           -> all suites ok
soldr cargo test -p fbuild-serial        -> unit tests ok (see note)
soldr cargo clippy -p fbuild-serial -p fbuild-deploy -p fbuild-cli -p fbuild-daemon --all-targets -- -D warnings  -> clean

Note: fbuild-serial boards.rs doctests (board_hint, family_for_vid_pid, vcom_for_env) fail locally without a populated FastLED/boards registry; this is identical on origin/main in this environment (this PR does not touch fbuild-serial or its dependencies) and green in CI.

Callers intentionally left on raw/lossy enumeration

Hardware

Not touched by this PR. The attached board (serial 5303284720C4641C) is deliberately preserved in the reproduced wedge (BOOTSEL RPI-RP2 on G:, phantom COM12, PICOBOOT MI_01 CM_PROB_FAILED_INSTALL) for the fbuild-only closeout run, which follows with #1152's --admin orchestration per the plan in #1152 (comment). Next child after merge: #1152.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Improved deploy recovery and monitoring by preferring the healthiest recovered post-flash runtime connection when available, and skipping post-flash monitoring when no healthy endpoint exists.
  • Bug Fixes
    • Prevented Raspberry Pi and RP2040/RP2350 runtime CDC selection from using known-unhealthy records, with clearer diagnostics for phantom/ambiguous devices and safer recovery behavior that avoids unsafe pre-flash substitutions.
    • Enhanced BOOTSEL CDC timeout/recovery guidance when flash succeeds but runtime CDC never becomes reachable.
  • Documentation
    • Updated WCHISP failure guidance to point to fbuild port scan for identifying the USB-ISP device.

…quisition

A retained CM_PROB_PHANTOM devnode whose serial still matches the board
could be selected pre-flash, touched at 1200 baud, and returned as the
post-flash deploy port. Health from #1146/#1150 now gates every step:

- rp2040_target: select_cdc_candidate returns a health-eligible
  PicoCdcPort (phantom/present-problem records are never selected);
  resolve_requested_runtime_target fails an explicit selector that
  matches a known-unhealthy record, with health/problem/instance detail
- rp2040: the post-flash wait adds a bounded openability probe seam;
  timeout diagnostics carry the last open error and phantom-aware
  BOOT/RESET recovery guidance; Deployer::owns_post_flash_port_discovery
  marks the RP2040 port as authoritative
- daemon: recovery/monitor port resolution no longer substitutes the
  requested pre-flash name when the deployer owns port discovery
  (deploy_port_str.or(result.port) inverted the trust order); flash
  success with an unrecovered CDC now surfaces the deployer's recovery
  diagnostic instead of discarding it; choose_deploy_port partitions
  known-unhealthy RP2040 records out of auto-selection with a
  diagnostic warning

Closes #1147

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6951133d-7f61-427c-9f62-ad164a6a659a

📥 Commits

Reviewing files that changed from the base of the PR and between 1589e7a and 3e7cb4d.

📒 Files selected for processing (3)
  • .github/workflows/dylint.yml
  • dylints/ban_raw_path_prefix_compare/src/allowlist.txt
  • dylints/ban_raw_path_prefix_compare/src/lib.rs

📝 Walkthrough

Walkthrough

The deployer contract now identifies post-flash port discovery ownership. RP2040 and Raspberry Pi CDC selection excludes known-unhealthy records, probes endpoint openability, reports richer diagnostics, and prevents monitoring from using stale pre-flash ports.

Changes

RP2040 CDC recovery and deploy selection

Layer / File(s) Summary
Healthy CDC candidate selection
crates/fbuild-deploy/src/rp2040_target.rs, crates/fbuild-daemon/src/handlers/operations/deploy_port.rs
Filters known-unhealthy CDC records from automatic selection, reports health details, and preserves healthy and unknown-health selection behavior.
Probe-confirmed post-flash CDC recovery
crates/fbuild-deploy/src/rp2040.rs
Adds bounded openability probing, tracks open failures, enriches timeout guidance, returns confirmed port values, and marks RP2040 discovery as deployer-owned.
Post-flash discovery ownership contract
crates/fbuild-deploy/src/lib.rs, crates/fbuild-daemon/src/handlers/operations/deploy.rs
Applies the ownership hook to recovery-port resolution, success messaging, monitoring, and legacy fallback behavior.
Supporting diagnostics and build updates
crates/fbuild-deploy/src/wchisp.rs, crates/fbuild-serial/..., crates/fbuild-config/..., crates/fbuild-toolchain/..., dylints/..., .github/workflows/dylint.yml
Updates failure guidance, documentation examples, platform warning attributes, formatting, test closure, lint imports and allowlists, and dylint alias refresh behavior.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant DeployHandler
  participant Rp2040Deployer
  participant CDCWait
  participant Monitor
  DeployHandler->>Rp2040Deployer: deploy and recover runtime CDC
  Rp2040Deployer->>CDCWait: select and probe CDC candidate
  CDCWait-->>Rp2040Deployer: confirmed endpoint or recovery diagnostics
  Rp2040Deployer-->>DeployHandler: deployment result and recovery port
  DeployHandler->>Monitor: monitor recovered endpoint or skip monitoring
Loading

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main RP2040 change: using Windows port health during selection and reacquisition.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/1147-rp2040-health-gate

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Checkov (3.3.8)
.github/workflows/dylint.yml

Traceback (most recent call last):
File "/usr/local/bin/checkov", line 2, in
from checkov.main import Checkov
ModuleNotFoundError: No module named 'checkov'


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/fbuild-daemon/src/handlers/operations/deploy.rs`:
- Around line 901-913: Update the recovery cleanup around resolve_recovery_port
and clear_preemption so deploy_port_str is cleared first when present, before
falling back to clearing recovery_port. Preserve the existing recovery-port
behavior for cases without a deploy port, ensuring owned post-flash recovery
releases the original pre-flash preemption even when the port is renumbered.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: a93469ac-0311-4871-8888-96b0c319bbbf

📥 Commits

Reviewing files that changed from the base of the PR and between 71096e2 and 40c138b.

📒 Files selected for processing (5)
  • crates/fbuild-daemon/src/handlers/operations/deploy.rs
  • crates/fbuild-daemon/src/handlers/operations/deploy_port.rs
  • crates/fbuild-deploy/src/lib.rs
  • crates/fbuild-deploy/src/rp2040.rs
  • crates/fbuild-deploy/src/rp2040_target.rs

Comment on lines +901 to 913
let port_discovery_owned = deployer_for_recovery
.as_ref()
.is_some_and(|deployer| deployer.owns_post_flash_port_discovery());
let recovery_port = resolve_recovery_port(
port_discovery_owned,
deploy_port_str.clone(),
deploy_result
.as_ref()
.ok()
.and_then(|result| result.port.clone())
});
.and_then(|result| result.port.clone()),
);
if let Some(ref p) = recovery_port {
ctx.serial_manager.clear_preemption(p).await;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -nP --type=rust -C4 '(fn\s+clear_preemption|fn\s+preempt_for_deploy|last_disconnect|remove.*preempt)' crates/fbuild-daemon/src

Repository: FastLED/fbuild

Length of output: 12569


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== deploy.rs relevant sections =="
sed -n '400,470p' crates/fbuild-daemon/src/handlers/operations/deploy.rs
sed -n '850,930p' crates/fbuild-daemon/src/handlers/operations/deploy.rs
sed -n '1210,1265p' crates/fbuild-daemon/src/handlers/operations/deploy.rs

echo "== serial_manager references in deploy and manager =="
rg -n --type=rust -C3 'serial_manager|preempt|clear_preemption|preemption' crates/fbuild-daemon/src/handlers/operations/deploy.rs crates/fbuild-daemon/src h 2>/dev/null || true

echo "== locate serial_manager type definition/mocks/tests =="
rg -n --type=rust -C3 'struct .*Serial|serial_manager|preempt_for_deploy|clear_preemption|MonitorLease|preemption' crates/fbuild-daemon/src

Repository: FastLED/fbuild

Length of output: 50370


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== shared serial manager files =="
fd -i 'serial' crates/fbuild-daemon/src -t f

echo "== serial manager outline and relevant preemption API =="
for f in $(fd -i 'serial' crates/fbuild-daemon/src -t f); do
  echo "--- $f"
  rg -n 'pub struct |mod |impl .*Serial|clear_preemption|preempt_for_deploy|has_clients|notify_port_renumbered|rebind_port_session' "$f" -C4
done

echo "== focused read files =="
sed -n '1,360p' crates/fbuild-daemon/src/handlers/operations/monitor.rs
sed -n '700,950p' crates/fbuild-daemon/src/handlers/operations/deploy.rs
sed -n '1220,1260p' crates/fbuild-daemon/src/handlers/operations/deploy.rs

Repository: FastLED/fbuild

Length of output: 26436


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files that define SharedSerialManager =="
rg -n --type=rust 'SharedSerialManager|impl .*SharedSerialManager|struct SharedSerialManager' crates -S

echo "== imports and usages with clearer boundary =="
rg -n --type=rust 'serial_manager|SerialManager|Monit' crates/fbuild-daemon/src/context.rs crates/fbuild-daemon/src/handlers/operations/deploy.rs crates/fbuild-daemon/src/handlers/operations/reset.rs crates/fbuild-daemon/src/handlers/operations/monitor.rs crates/fbuild-daemon/src/lib.rs crates/fbuild-daemon/src/main.rs -S

echo "== repository-wide serial manager exact matches =="
rg -n 'struct .*SerialManager|enum .*SerialManager|impl .*SerialManager|clear_preemption|preempt_for_deploy|notify_port_renumbered' crates -S

Repository: FastLED/fbuild

Length of output: 12021


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== manager preemption, client tracking, and renumber APIs =="
sed -n '50,140p' crates/fbuild-serial/src/manager.rs
sed -n '440,520p' crates/fbuild-serial/src/manager.rs
sed -n '640,730p' crates/fbuild-serial/src/manager.rs
sed -n '840,945p' crates/fbuild-serial/src/manager.rs
sed -n '1020,1105p' crates/fbuild-serial/src/manager.rs

echo "== monitor attach/detach/close preemption handling =="
rg -n --type=rust -C5 'attach_reader|detach_reader|release_writer|close_port_after_grace_if_idle|has_clients' crates/fbuild-serial/src/manager.rs

echo "== local behavioral verifier for preemption mapping logic =="
python3 - <<'PY'
requested = "COM12"
deployer_port = "COM27"
old_recovered = requested or deployer_port
new_recovered = deployer_port
preempted = requested
cleared_by_new = set(p for p in [deployer_port] if p)
print("preempted:", preempted)
print("ownership: requested", requested, "deployer_port", deployer_port)
print("old_recovery_port:", old_recovered)
print("new_recovery_port:", new_recovered)
print("cleared_preempted", preempted in cleared_by_new)
PY

Repository: FastLED/fbuild

Length of output: 22357


Release the pre-flash preemption after owned post-flash recovery. Owned deployers resolve recovery_port to the new port, so a renumbered pre-flash endpoint like COM12 never has its preempt_for_deploy() lease cleared when clear_preemption only runs on the recovered COM27 port. Clear deploy_port_str before falling back to recovery-port clearing.

🔒 Proposed fix
     let recovery_port = resolve_recovery_port(
         port_discovery_owned,
         deploy_port_str.clone(),
         deploy_result
             .as_ref()
             .ok()
             .and_then(|result| result.port.clone()),
     );
+    // Always release the preemption taken on the pre-flash port,
+    // even when owned discovery moved recovery to a renumbered endpoint.
+    if let Some(ref pre) = deploy_port_str {
+        if recovery_port.as_deref() != Some(pre.as_str()) {
+            ctx.serial_manager.clear_preemption(pre).await;
+        }
+    }
     if let Some(ref p) = recovery_port {
         ctx.serial_manager.clear_preemption(p).await;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let port_discovery_owned = deployer_for_recovery
.as_ref()
.is_some_and(|deployer| deployer.owns_post_flash_port_discovery());
let recovery_port = resolve_recovery_port(
port_discovery_owned,
deploy_port_str.clone(),
deploy_result
.as_ref()
.ok()
.and_then(|result| result.port.clone())
});
.and_then(|result| result.port.clone()),
);
if let Some(ref p) = recovery_port {
ctx.serial_manager.clear_preemption(p).await;
let port_discovery_owned = deployer_for_recovery
.as_ref()
.is_some_and(|deployer| deployer.owns_post_flash_port_discovery());
let recovery_port = resolve_recovery_port(
port_discovery_owned,
deploy_port_str.clone(),
deploy_result
.as_ref()
.ok()
.and_then(|result| result.port.clone()),
);
// Always release the preemption taken on the pre-flash port,
// even when owned discovery moved recovery to a renumbered endpoint.
if let Some(ref pre) = deploy_port_str {
if recovery_port.as_deref() != Some(pre.as_str()) {
ctx.serial_manager.clear_preemption(pre).await;
}
}
if let Some(ref p) = recovery_port {
ctx.serial_manager.clear_preemption(p).await;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/fbuild-daemon/src/handlers/operations/deploy.rs` around lines 901 -
913, Update the recovery cleanup around resolve_recovery_port and
clear_preemption so deploy_port_str is cleared first when present, before
falling back to clearing recovery_port. Preserve the existing recovery-port
behavior for cases without a deploy port, ensuring owned post-flash recovery
releases the original pre-flash preemption even when the port is renumbered.

zackees and others added 10 commits July 22, 2026 23:44
… gate

The Formatting and Check workflows have been red on main since #1145:
- four files (two from this branch, two pre-existing: board/loaders.rs,
  ch32v_core.rs) carry diffs under the pinned 1.94.1 formatter. Local
  cargo-fmt --all silently skips the daemon handler files on this
  checkout, so they were formatted with direct invocations.
- the #1150 health-classification helpers in fbuild-serial have no
  non-Windows, non-test caller, which RUSTFLAGS=-D warnings promotes to
  a hard error on the ubuntu/macos Check jobs. Annotate them
  cfg_attr(not(windows), allow(dead_code)) — they are fed by the
  Windows PnP enumeration and unit-tested cross-platform.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
board_hint, vcom_for_env, and family_for_vid_pid resolve through the
FastLED/boards registry at runtime; on doctest hosts (local and CI)
that cache is absent, so the three examples panicked on every Check
job since the vendored-table migration. no_run keeps them
compile-checked without requiring a populated registry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ci/find_direct_subprocess.py --fail has flagged the ch32v multilib
probe since it landed, keeping the Check jobs red. The spawn is a
short synchronous -print-multi-directory capability probe; mark it
allow-direct-spawn rather than routing a one-flag probe through the
subprocess facade.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The USB VID/PID catalogue-literal guard has flagged this user-facing
string since the wchisp deployer landed, keeping Check (ubuntu) red.
Identity data belongs to the FastLED/boards registry; point the user
at 'fbuild port scan' instead of embedding 4348/1a86 literals.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Dylint crate-formatting gate (nightly style edition) has wanted
these two use-statement reorders since the lint landed; apply them so
the gate goes green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… dylint

Same nightly-2026-03-26 style-edition reorders as ban_raw_subprocess;
verified with the crate-pinned formatter that both format-checked
dylint crates are now clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…lint

Companion to the Python-lint marker: the workspace dylint pass wants
its own allowlist.txt entry for the same short synchronous
-print-multi-directory capability probe. ci/check_dylint_allowlists.py
passes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ix_compare

WalkDir-derived paths are stripped against the exact root spelling they
were enumerated from (same-normalized by construction) and feed a
content hash, not a path-keyed cache lookup — the lint's own stated
exemption. Pre-existing debt unmasked once the earlier Dylint gate
failures were fixed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… edit

The previous round still flagged the freshly-allowlisted esp32
framework-library cache: the workspace dylint step served a compiled
lint from the restored build cache, ignoring the allowlist-only edit.
Touch the lint source (documenting the trap) so the fingerprint moves.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cargo-dylint loads the <name>@<toolchain> library copy, but the alias
loop only created it when absent. With target caching, a restored stale
alias therefore pinned every lint to its first-ever compiled allowlist:
rebuilt bare libraries (allowlist/source edits) were never loaded, which
is why ban_raw_path_prefix_compare kept flagging the freshly-allowlisted
esp32 framework-library cache. Overwrite the alias when the bare library
is newer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Triage

Development

Successfully merging this pull request may close these issues.

fix(rp2040): reject phantom CDC ports and give recoverable BOOTSEL guidance

1 participant