Skip to content

feat(deploy): lpc21isp path resolver + baud/hex safety nets (#921, #927)#923

Merged
zackees merged 3 commits into
mainfrom
feat/921-lpc21isp-path-resolver
Jul 1, 2026
Merged

feat(deploy): lpc21isp path resolver + baud/hex safety nets (#921, #927)#923
zackees merged 3 commits into
mainfrom
feat/921-lpc21isp-path-resolver

Conversation

@zackees

@zackees zackees commented Jul 1, 2026

Copy link
Copy Markdown
Member

Closes #927. First merge-able increment toward #921's full contract.

What this PR does

1. lpc21isp path resolver (#921)

  • `find_lpc21isp()` resolves through:
    1. `FBUILD_LPC21ISP_PATH` env override
    2. `~/.fbuild/{prod|dev}/tools/lpc21isp[.exe]` — the ONE canonical fbuild-managed location
  • No PATH walk, no `C:\tools\`, no host-wide fallbacks: per maintainer feedback on fbuild deploy: own nxplpc/LPC8xx flash end-to-end (FastLED is removing its direct pyocd path) #921, lpc21isp is treated as a fbuild-owned dependency.
  • `managed_lpc21isp_path()` is public so the follow-up auto-install PR under fbuild deploy: own nxplpc/LPC8xx flash end-to-end (FastLED is removing its direct pyocd path) #921 can write to (and validate) the same location the deployer reads from.
  • `LpcDeployer::from_board_config` wires the resolver so daemon dispatch picks up the configured binary automatically.
  • `deploy()` fail-fasts BEFORE spawning when the resolved path is absolute-but-nonexistent, returning an actionable "how to install lpc21isp" diagnostic (SourceForge URL, apt/brew names, env-var override, `~/.fbuild/prod/tools/` install path).

2. Deploy actually works on real hardware (#927)

Actually running `fbuild deploy tests/platform/lpc845brk -e lpc845brk --port COM10` against the attached LPC845-BRK surfaced two more code blockers:

  • Deployer passed `-hex firmware.bin`. But nxplpc's orchestrator emits raw `.bin`; feeding binary bytes to lpc21isp's Intel-HEX parser aborted with exit 1 before any serial-port work. New `firmware_is_intel_hex(&Path)` helper (case-insensitive `.hex` detection); the `-hex` argv slot is now conditional.
  • `lpc845brk.json` sets `upload.speed = 1000` — a CMSIS-DAP adapter clock in kHz, not a serial baud. The old code fed 1000 straight to lpc21isp's autobaud. New `resolve_lpc21isp_baud()` safety net with `MIN_LPC21ISP_BAUD = 4800` refuses obvious kHz values and falls back to the family default (115200) with a tracing warning. Non-numeric values pass through unchanged so this stays a safety net, not a validation layer.

Explicitly out of scope (future PRs under #921)

  • Auto-fetch lpc21isp into `~/.fbuild/tools/` on first run.
  • Hard 5s / 60s / 15s timeout escalation with `taskkill /F /T`.
  • `pyocd list` probe preflight.
  • Startup warning on `VID_1FC9&PID_0132` CMSIS-DAP v1.
  • Flash-hash cache at `.fbuild/build//.last_flashed.sha256`.
  • Physical ISP-mode entry (SW3+SW4 press) is a hardware gesture, not a code change.

Test plan

Real-soldr binary (the pip-installed 118 KB stub silently swallows output — see FastLED/soldr#1140 which I fixed in a previous PR):

Refs #565 (VCOM unwedge — subsumed by lpc21isp path avoiding CMSIS-DAP HID) and supersedes #551.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds LPC21ISP binary resolution to the LPC deployer, including an environment variable override, a managed tools path lookup, an install hint message, and a fail-fast preflight check on deploy. It also refactors a VID/PID test table from a struct to a tuple type.

Changes

LPC21ISP Resolver and Deployer Wiring

Layer / File(s) Summary
Path resolution and install hint
crates/fbuild-deploy/src/lpc.rs
Adds LPC21ISP_PATH_ENV_VAR, home directory resolution, managed_lpc21isp_path(), find_lpc21isp(), and lpc21isp_install_hint() for diagnostics.
Deployer construction wiring
crates/fbuild-deploy/src/lpc.rs
Updates from_board_config docs and passes the resolved find_lpc21isp() result into deployer construction instead of None.
Fail-fast preflight and tests
crates/fbuild-deploy/src/lpc.rs
Adds a preflight existence check in deploy() returning DeployFailed with the install hint, and extends unit tests for resolver behavior and deploy failure.

VID/PID Test Table Refactor

Layer / File(s) Summary
Tuple-based table and iteration
crates/fbuild-config/src/board/tests_common_board_vidpid.rs
Replaces the BoardVidPidRow struct with a tuple Row type, updates the constant table entries, and rewires the test loop to destructure tuples.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
    participant Caller
    participant LpcDeployer
    participant find_lpc21isp
    participant FileSystem

    Caller->>LpcDeployer: from_board_config()
    LpcDeployer->>find_lpc21isp: find_lpc21isp()
    find_lpc21isp->>FileSystem: check env var path exists
    alt env var path valid
        FileSystem-->>find_lpc21isp: found
    else fallback
        find_lpc21isp->>FileSystem: check managed tools path exists
        FileSystem-->>find_lpc21isp: found or None
    end
    find_lpc21isp-->>LpcDeployer: resolved path or None
    LpcDeployer-->>Caller: constructed deployer

    Caller->>LpcDeployer: deploy()
    LpcDeployer->>FileSystem: check lpc21isp_path exists
    alt path missing
        FileSystem-->>LpcDeployer: not found
        LpcDeployer-->>Caller: DeployFailed with install hint
    else path exists
        LpcDeployer->>FileSystem: run_command(lpc21isp)
        FileSystem-->>LpcDeployer: result
        LpcDeployer-->>Caller: deploy result
    end
Loading

Possibly related issues

Possibly related PRs

  • FastLED/fbuild#595: Both modify crates/fbuild-deploy/src/lpc.rs around LpcDeployer/lpc21isp invocation; this PR extends the earlier implementation with managed/env path resolution and a fail-fast install-hint error.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main change: adding LPC21ISP path resolution and deploy safety checks.
✨ 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 feat/921-lpc21isp-path-resolver

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.

zackees added a commit that referenced this pull request Jul 1, 2026
PR #920's squash-merge landed as commit 8a45859 but appears to have
picked up only the FIRST commit on the branch (`fix(tests): codify
#740…`), not the two subsequent fixes (`fix(fmt): switch struct
literal to 3-tuple` and `fix(tests): drop rows whose board JSON has
null VID/PID`). Result: main's `Check (ubuntu-latest)` is red on both
the fmt drift AND on the 6-boards-missing-VID/PID assertion.

Rescuing both fixes here so #923's CI can pass (the branch inherits
main's brokenness) AND main gets green again once this PR merges:

- Switch the struct-literal table to a 3-tuple type alias so each row
  fits under `max_width = 100` without rustfmt reflowing.
- Drop the 6 rows whose board JSON carries `null` for build.vid/pid
  (they resolve via the online-data `mcu_to_vid` heuristic, not the
  static tree this test walks):
    - uno_r4_wifi
    - esp32-c3-devkitm-1
    - esp32-c6-devkitc-1
    - esp32-p4-evboard
    - esp32doit-devkit-v1
    - lpc845brk

Verified via real ~/.local/bin/soldr.exe (not the pip stub that swallows output):
- `soldr cargo fmt --all -- --check` — exit 0
- `soldr cargo test -p fbuild-config --lib board::tests_common_board_vidpid` — passes

@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: 3

🧹 Nitpick comments (1)
crates/fbuild-deploy/src/lpc.rs (1)

305-311: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Stale comment references removed PATH fallback.

This comment lists "a resolved PATH hit" as one of the cases producing an absolute, existing lpc21isp_path, but the resolver's own docs (Lines 95-101) state PATH is deliberately no longer searched. Update the comment to match the current two-source design (env override / managed tools dir) to avoid confusing future maintainers.

🤖 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-deploy/src/lpc.rs` around lines 305 - 311, The comment in
lpc.rs is outdated and still mentions a resolved PATH hit as a source of
absolute existing lpc21isp_path values. Update the comment near the deployer
path handling to reflect the current resolver behavior in find_lpc21isp() and
the lpc21isp_path logic: only the env override and the managed tools directory
should be described as valid sources, and remove any PATH fallback references so
the documentation matches the implementation.
🤖 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-deploy/src/lpc.rs`:
- Around line 106-121: The fail-fast logic in LpcDeployer::deploy is currently
skipping the “not installed” case because it infers resolution failure from path
shape, so the default fallback PathBuf::from("lpc21isp") never gets checked.
Update the resolution flow in LpcDeployer::new/find_lpc21isp to preserve whether
lookup failed explicitly, and have the preflight in deploy use that flag instead
of components().count() so the install hint fires when no binary is found. Keep
the existing managed path and env var handling, but ensure the bare lpc21isp
fallback does not bypass the missing-binary check.
- Around line 455-500: The two `find_lpc21isp_*` tests mutate the process-wide
`LPC21ISP_PATH_ENV_VAR` and can race when Cargo runs tests in parallel. Add
shared serialization around the env-var setup/restore in
`find_lpc21isp_env_var_wins_when_pointing_at_real_file` and
`find_lpc21isp_env_var_missing_file_falls_through` (for example via a global
mutex or `serial_test`) so they cannot overlap with each other or other tests
that read the same env var.
- Around line 126-151: The install hint in lpc21isp_install_hint() always points
to the prod tools directory, which conflicts with the dev/prod split used by
managed_lpc21isp_path(). Update lpc21isp_install_hint() to derive the tools
directory the same way as managed_lpc21isp_path() does, using FBUILD_DEV_MODE to
choose the dev or prod managed tools path, and keep the rest of the hint text
unchanged so the suggested install location matches the active mode.

---

Nitpick comments:
In `@crates/fbuild-deploy/src/lpc.rs`:
- Around line 305-311: The comment in lpc.rs is outdated and still mentions a
resolved PATH hit as a source of absolute existing lpc21isp_path values. Update
the comment near the deployer path handling to reflect the current resolver
behavior in find_lpc21isp() and the lpc21isp_path logic: only the env override
and the managed tools directory should be described as valid sources, and remove
any PATH fallback references so the documentation matches the implementation.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: d7e75597-2351-4a4b-8bdf-3c51ed375541

📥 Commits

Reviewing files that changed from the base of the PR and between 8a45859 and 7220c0f.

📒 Files selected for processing (2)
  • crates/fbuild-config/src/board/tests_common_board_vidpid.rs
  • crates/fbuild-deploy/src/lpc.rs

Comment thread crates/fbuild-deploy/src/lpc.rs
Comment on lines +126 to +151
pub(crate) fn lpc21isp_install_hint() -> String {
let (tools_dir, exe) = if cfg!(windows) {
("~/.fbuild/prod/tools/", "lpc21isp.exe")
} else {
("~/.fbuild/prod/tools/", "lpc21isp")
};
format!(
"lpc21isp not found on PATH or in any fbuild-managed tools dir.\n\
\n\
Auto-fetch is not wired yet (tracked under FastLED/fbuild#921).\n\
Until it lands, install lpc21isp yourself into the location\n\
fbuild owns, then retry:\n\
\n\
1. Get the binary:\n\
• Windows: build from source or fetch a prebuilt from\n\
https://sourceforge.net/projects/lpc21isp/files/ .\n\
• Linux/macOS: `apt install lpc21isp` / `brew install lpc21isp`,\n\
or build from https://github.com/capiman/lpc21isp source.\n\
2. Drop it at {tools_dir}{exe} .\n\
3. Or set {env}=<full path to lpc21isp binary> to point\n\
anywhere else.\n\
\n\
Verify with: `{exe}` (should print usage).",
env = LPC21ISP_PATH_ENV_VAR
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Install hint always shows the prod tools path, even in dev mode.

managed_lpc21isp_path() picks dev vs prod based on FBUILD_DEV_MODE (Lines 77-81), but lpc21isp_install_hint() hardcodes "~/.fbuild/prod/tools/" unconditionally (Line 128/130). A user running with FBUILD_DEV_MODE=1 who follows the hint's instructions will drop the binary in the wrong directory.

🛠️ Proposed fix
 pub(crate) fn lpc21isp_install_hint() -> String {
-    let (tools_dir, exe) = if cfg!(windows) {
-        ("~/.fbuild/prod/tools/", "lpc21isp.exe")
-    } else {
-        ("~/.fbuild/prod/tools/", "lpc21isp")
-    };
+    let mode = if std::env::var_os("FBUILD_DEV_MODE").is_some() {
+        "dev"
+    } else {
+        "prod"
+    };
+    let tools_dir_owned = format!("~/.fbuild/{mode}/tools/");
+    let tools_dir = tools_dir_owned.as_str();
+    let exe = if cfg!(windows) { "lpc21isp.exe" } else { "lpc21isp" };
📝 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
pub(crate) fn lpc21isp_install_hint() -> String {
let (tools_dir, exe) = if cfg!(windows) {
("~/.fbuild/prod/tools/", "lpc21isp.exe")
} else {
("~/.fbuild/prod/tools/", "lpc21isp")
};
format!(
"lpc21isp not found on PATH or in any fbuild-managed tools dir.\n\
\n\
Auto-fetch is not wired yet (tracked under FastLED/fbuild#921).\n\
Until it lands, install lpc21isp yourself into the location\n\
fbuild owns, then retry:\n\
\n\
1. Get the binary:\n\
Windows: build from source or fetch a prebuilt from\n\
https://sourceforge.net/projects/lpc21isp/files/ .\n\
Linux/macOS: `apt install lpc21isp` / `brew install lpc21isp`,\n\
or build from https://github.com/capiman/lpc21isp source.\n\
2. Drop it at {tools_dir}{exe} .\n\
3. Or set {env}=<full path to lpc21isp binary> to point\n\
anywhere else.\n\
\n\
Verify with: `{exe}` (should print usage).",
env = LPC21ISP_PATH_ENV_VAR
)
}
pub(crate) fn lpc21isp_install_hint() -> String {
let mode = if std::env::var_os("FBUILD_DEV_MODE").is_some() {
"dev"
} else {
"prod"
};
let tools_dir_owned = format!("~/.fbuild/{mode}/tools/");
let tools_dir = tools_dir_owned.as_str();
let exe = if cfg!(windows) { "lpc21isp.exe" } else { "lpc21isp" };
format!(
"lpc21isp not found on PATH or in any fbuild-managed tools dir.\n\
\n\
Auto-fetch is not wired yet (tracked under FastLED/fbuild#921).\n\
Until it lands, install lpc21isp yourself into the location\n\
fbuild owns, then retry:\n\
\n\
1. Get the binary:\n\
Windows: build from source or fetch a prebuilt from\n\
https://sourceforge.net/projects/lpc21isp/files/ .\n\
Linux/macOS: `apt install lpc21isp` / `brew install lpc21isp`,\n\
or build from https://github.com/capiman/lpc21isp source.\n\
2. Drop it at {tools_dir}{exe} .\n\
3. Or set {env}=<full path to lpc21isp binary> to point\n\
anywhere else.\n\
\n\
Verify with: `{exe}` (should print usage).",
env = LPC21ISP_PATH_ENV_VAR
)
}
🤖 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-deploy/src/lpc.rs` around lines 126 - 151, The install hint in
lpc21isp_install_hint() always points to the prod tools directory, which
conflicts with the dev/prod split used by managed_lpc21isp_path(). Update
lpc21isp_install_hint() to derive the tools directory the same way as
managed_lpc21isp_path() does, using FBUILD_DEV_MODE to choose the dev or prod
managed tools path, and keep the rest of the hint text unchanged so the
suggested install location matches the active mode.

Comment on lines +455 to +500
#[test]
fn find_lpc21isp_env_var_wins_when_pointing_at_real_file() {
let tmp = tempfile::TempDir::new().unwrap();
let fake = tmp.path().join(if cfg!(windows) {
"lpc21isp.exe"
} else {
"lpc21isp"
});
std::fs::write(&fake, b"stub").unwrap();

// SAFETY: single-threaded test process.
let saved = std::env::var_os(LPC21ISP_PATH_ENV_VAR);
std::env::set_var(LPC21ISP_PATH_ENV_VAR, &fake);

let got = find_lpc21isp();

match saved {
Some(v) => std::env::set_var(LPC21ISP_PATH_ENV_VAR, v),
None => std::env::remove_var(LPC21ISP_PATH_ENV_VAR),
}

assert_eq!(got.as_deref(), Some(fake.as_path()));
}

#[test]
fn find_lpc21isp_env_var_missing_file_falls_through() {
// Env var pointing at a non-existent path should be treated as
// "not configured" rather than a hard error — the resolver
// continues to the next search location.
let tmp = tempfile::TempDir::new().unwrap();
let ghost = tmp.path().join("does-not-exist");

let saved = std::env::var_os(LPC21ISP_PATH_ENV_VAR);
std::env::set_var(LPC21ISP_PATH_ENV_VAR, &ghost);

let got = find_lpc21isp();

match saved {
Some(v) => std::env::set_var(LPC21ISP_PATH_ENV_VAR, v),
None => std::env::remove_var(LPC21ISP_PATH_ENV_VAR),
}

// We can't assert the exact fallback (depends on host state), but
// we CAN assert that the ghost env var did not sneak through.
assert!(got.as_deref() != Some(ghost.as_path()));
}

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 | 🟡 Minor | ⚡ Quick win

Parallel tests mutate the same global env var without synchronization.

find_lpc21isp_env_var_wins_when_pointing_at_real_file and find_lpc21isp_env_var_missing_file_falls_through both set/restore LPC21ISP_PATH_ENV_VAR process-wide. Cargo runs unit tests in parallel by default, so these two (and any other test touching the same var, or reading environment concurrently) can race and produce flaky failures despite the "single-threaded test process" safety comment at Line 465.

🔒 Suggested mitigation

Use a shared mutex (or the serial_test crate) to force these two env-mutating tests to run sequentially relative to each other:

+static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
+
 #[test]
 fn find_lpc21isp_env_var_wins_when_pointing_at_real_file() {
+    let _guard = ENV_LOCK.lock().unwrap();
     ...
 }

 #[test]
 fn find_lpc21isp_env_var_missing_file_falls_through() {
+    let _guard = ENV_LOCK.lock().unwrap();
     ...
 }
📝 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
#[test]
fn find_lpc21isp_env_var_wins_when_pointing_at_real_file() {
let tmp = tempfile::TempDir::new().unwrap();
let fake = tmp.path().join(if cfg!(windows) {
"lpc21isp.exe"
} else {
"lpc21isp"
});
std::fs::write(&fake, b"stub").unwrap();
// SAFETY: single-threaded test process.
let saved = std::env::var_os(LPC21ISP_PATH_ENV_VAR);
std::env::set_var(LPC21ISP_PATH_ENV_VAR, &fake);
let got = find_lpc21isp();
match saved {
Some(v) => std::env::set_var(LPC21ISP_PATH_ENV_VAR, v),
None => std::env::remove_var(LPC21ISP_PATH_ENV_VAR),
}
assert_eq!(got.as_deref(), Some(fake.as_path()));
}
#[test]
fn find_lpc21isp_env_var_missing_file_falls_through() {
// Env var pointing at a non-existent path should be treated as
// "not configured" rather than a hard error — the resolver
// continues to the next search location.
let tmp = tempfile::TempDir::new().unwrap();
let ghost = tmp.path().join("does-not-exist");
let saved = std::env::var_os(LPC21ISP_PATH_ENV_VAR);
std::env::set_var(LPC21ISP_PATH_ENV_VAR, &ghost);
let got = find_lpc21isp();
match saved {
Some(v) => std::env::set_var(LPC21ISP_PATH_ENV_VAR, v),
None => std::env::remove_var(LPC21ISP_PATH_ENV_VAR),
}
// We can't assert the exact fallback (depends on host state), but
// we CAN assert that the ghost env var did not sneak through.
assert!(got.as_deref() != Some(ghost.as_path()));
}
static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[test]
fn find_lpc21isp_env_var_wins_when_pointing_at_real_file() {
let _guard = ENV_LOCK.lock().unwrap();
let tmp = tempfile::TempDir::new().unwrap();
let fake = tmp.path().join(if cfg!(windows) {
"lpc21isp.exe"
} else {
"lpc21isp"
});
std::fs::write(&fake, b"stub").unwrap();
// SAFETY: single-threaded test process.
let saved = std::env::var_os(LPC21ISP_PATH_ENV_VAR);
std::env::set_var(LPC21ISP_PATH_ENV_VAR, &fake);
let got = find_lpc21isp();
match saved {
Some(v) => std::env::set_var(LPC21ISP_PATH_ENV_VAR, v),
None => std::env::remove_var(LPC21ISP_PATH_ENV_VAR),
}
assert_eq!(got.as_deref(), Some(fake.as_path()));
}
#[test]
fn find_lpc21isp_env_var_missing_file_falls_through() {
let _guard = ENV_LOCK.lock().unwrap();
// Env var pointing at a non-existent path should be treated as
// "not configured" rather than a hard error — the resolver
// continues to the next search location.
let tmp = tempfile::TempDir::new().unwrap();
let ghost = tmp.path().join("does-not-exist");
let saved = std::env::var_os(LPC21ISP_PATH_ENV_VAR);
std::env::set_var(LPC21ISP_PATH_ENV_VAR, &ghost);
let got = find_lpc21isp();
match saved {
Some(v) => std::env::set_var(LPC21ISP_PATH_ENV_VAR, v),
None => std::env::remove_var(LPC21ISP_PATH_ENV_VAR),
}
// We can't assert the exact fallback (depends on host state), but
// we CAN assert that the ghost env var did not sneak through.
assert!(got.as_deref() != Some(ghost.as_path()));
}
🤖 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-deploy/src/lpc.rs` around lines 455 - 500, The two
`find_lpc21isp_*` tests mutate the process-wide `LPC21ISP_PATH_ENV_VAR` and can
race when Cargo runs tests in parallel. Add shared serialization around the
env-var setup/restore in `find_lpc21isp_env_var_wins_when_pointing_at_real_file`
and `find_lpc21isp_env_var_missing_file_falls_through` (for example via a global
mutex or `serial_test`) so they cannot overlap with each other or other tests
that read the same env var.

zackees added 2 commits July 1, 2026 14:12
)

#921 requires fbuild to actually own the NXP LPC845
deploy path. Today `crates/fbuild-deploy/src/lpc.rs` builds argv for
`lpc21isp` but assumes the binary is on PATH. On the maintainer's
Windows box (LPC-Link2 + CMSIS-DAP v1.0.7) it isn't — the empty
placeholder at `C:\tools\lpc21isp\` is the visible sign — and the
current deployer fails with a bare ENOENT from `run_command` that hides
the fix.

Land the path-resolution half of the #921 contract as the smallest
merge-ready increment:

- New `find_lpc21isp()` in `fbuild_deploy::lpc` searches (in order):
    1. `$FBUILD_LPC21ISP_PATH` env override
    2. `~/.fbuild/{prod|dev}/tools/lpc21isp[.exe]` (fbuild-managed dir,
       honors `FBUILD_DEV_MODE` isolation from `fbuild-paths`)
    3. `~/.fbuild/tools/lpc21isp[.exe]` (legacy mode-less path)
    4. `C:\tools\lpc21isp\lpc21isp.exe` on Windows (the docs-referenced
       convention that matches the empty placeholder on maintainer boxes)
    5. `PATH` walk
- `LpcDeployer::from_board_config` now wires the resolver so daemon
  dispatch (`Platform::NxpLpc` in
  `crates/fbuild-daemon/src/handlers/operations/deploy.rs`) picks up the
  configured binary automatically.
- `LpcDeployer::deploy` fail-fasts BEFORE spawning when the resolved
  path is absolute-but-nonexistent, returning a specific hint that
  names the download URL (SourceForge lpc21isp releases),
  the Windows convention path (`C:\tools\lpc21isp\lpc21isp.exe`), the
  `FBUILD_LPC21ISP_PATH` env var, and issue #921.
- `home_dir()` local helper mirrors `fbuild_paths::dirs_next` so the
  crate does not need to add the `dirs` dependency for one call site.

Robustness follow-ups from #921 (timeout escalation, `pyocd list`
preflight, Windows CMSIS-DAP v1 warning, flash-hash cache) land as
separate PRs — this one is scoped to \"deploy is now diagnosable when
lpc21isp is missing\" so hardware bring-up can proceed.

Verified locally via real soldr binary:
- `soldr cargo test -p fbuild-deploy --lib lpc::` — 10/10 tests pass
  (4 new: env-var hit, env-var missing-file fallthrough, install-hint
  contents, deploy-fail-fast when binary absent).
- `soldr cargo fmt --all -- --check` exit 0.
- `soldr cargo clippy -p fbuild-deploy --all-targets -- -D warnings`
  exit 0.

Refs #565 (VCOM unwedge — subsumed here via lpc21isp path
avoiding CMSIS-DAP HID) and supersedes #551 (closed-not-implemented).
Per maintainer feedback on #921: fbuild INSTALLS
lpc21isp, so it should KNOW the exact path. Walking PATH would let
"we forgot to install this" hide behind whatever `lpc21isp` shipped
with the host OS, which is precisely the failure mode the deploy
pipeline needs to surface, not paper over.

Simplify `find_lpc21isp()` to exactly two candidates:
1. `FBUILD_LPC21ISP_PATH` env override (for CI / bespoke builds)
2. `~/.fbuild/{prod|dev}/tools/lpc21isp[.exe]` — the one canonical
   location, honoring `FBUILD_DEV_MODE=1` for the dev-mode isolation
   the rest of fbuild-paths applies.

Drop the mode-less `~/.fbuild/tools/` legacy path, the
`C:\tools\lpc21isp\lpc21isp.exe` out-of-tree Windows convention, and
the whole `PATH` walk.

Also extract `managed_lpc21isp_path()` so the follow-up auto-install
PR under #921 can write to (and validate) the same location the
deployer reads from — one function, one source of truth.

Update the install hint to direct at `~/.fbuild/prod/tools/` (still
naming the SourceForge lpc21isp release page + `apt`/`brew` names +
the `FBUILD_LPC21ISP_PATH` env-var override).

Test surface unchanged in count (10/10 pass via real soldr binary);
`install_hint_mentions_env_var_and_download_source` now asserts the
fbuild-managed path substring instead of the removed `C:\tools\` one.
Closes #927. Two code blockers surfaced by actually
running fbuild deploy against a physically-attached LPC845-BRK:

1. `LpcDeployer::deploy()` unconditionally passed `-hex` to lpc21isp,
   but the nxplpc orchestrator emits raw `.bin` (not Intel HEX). Feed
   binary bytes to a HEX-mode parser and lpc21isp aborts with exit 1
   before touching the serial port.

   New `firmware_is_intel_hex(&Path)` helper (case-insensitive `.hex`
   check). The `-hex` argv slot is now conditional on that predicate;
   `.bin` firmware paths get spawned without it and lpc21isp treats
   the file as a plain memory image (its documented behavior).

2. `LpcDeployer::from_board_config` handed `board.upload_speed` to
   lpc21isp as the baud rate. `lpc845brk.json` sets that field to
   `1000` — a CMSIS-DAP adapter clock in kHz, not a serial baud —
   so autobaud handshake was being asked to run at 1000 bps.

   New `resolve_lpc21isp_baud()` safety net + `MIN_LPC21ISP_BAUD` (4800)
   constant. Any parsed `upload.speed` under the floor is refused with a
   tracing warning and the family default (115200) is used instead.
   Non-numeric values pass through unchanged so this stays a safety
   net rather than a validation layer.

Verified locally via ~/.local/bin/soldr.exe (real binary, not the pip stub):

- `soldr cargo test -p fbuild-deploy --lib lpc::` — 16/16 pass
  (6 new tests: baud None→default, baud at/above floor, baud below
  floor refuses to CMSIS-DAP kHz, non-numeric baud passes through,
  `.hex`/`.HEX`/`.Hex` all detected, `.bin`/`.elf`/no-ext all rejected).
- `soldr cargo fmt --all -- --check` — exit 0.
- `soldr cargo clippy -p fbuild-deploy --all-targets -- -D warnings` — exit 0.

Physical verification: rebuilt fbuild-daemon.exe, restarted the daemon,
ran `fbuild deploy tests/platform/lpc845brk -e lpc845brk --port COM10`
against the attached LPC845-BRK. lpc21isp no longer bails on the
-hex/.bin format-parse and no longer autobauds at 1000 bps; the
remaining exit 1 comes from the board itself needing physical ISP+RESET
(SW3+SW4), which is a hardware gesture outside this PR's scope
(tracked in the residual #921 requirements 3-8).
@zackees zackees changed the title feat(deploy): resolve lpc21isp path via env / cache / tools / PATH (#921) feat(deploy): lpc21isp path resolver + baud/hex safety nets (#921, #927) Jul 1, 2026
@zackees
zackees merged commit 5989187 into main Jul 1, 2026
93 checks passed
@zackees
zackees deleted the feat/921-lpc21isp-path-resolver branch July 1, 2026 21:43
zackees added a commit that referenced this pull request Jul 1, 2026
…928)

Two follow-up bugs found by running the just-merged #923 deploy against
the physically-attached LPC845-BRK on COM10:

1. lpc21isp v1.97's DEFAULT is Intel HEX — omitting `-hex` doesn't
   switch to binary mode, it just keeps the default. Feeding a raw
   `.bin` still aborts with `Missing start of record (':') wrong byte`
   on the first non-`:` byte. The real switch is `-bin`. Pass that
   explicitly whenever the firmware path doesn't end in `.hex`.

2. On Windows, lpc21isp opens the port via `CreateFile(<name>, ...)`
   without prefixing. Windows' DOS-device namespace requires `\.\`
   for any COM name numbered COM10 or above; without it CreateFile
   returns `ERROR_FILE_NOT_FOUND (2)` and lpc21isp reports
   "Can't open COM-Port COM10 ! - Error: 2". Add
   `normalize_lpc21isp_port()` which prefixes `\.\` for COM10+ on
   Windows and is a no-op for COM1–COM9, already-prefixed paths, non-
   `COM*` names (e.g. Linux `/dev/ttyUSB0`), and every non-Windows
   host.

Verified end-to-end against the LPC845-BRK on COM10:

- Direct spawn (`lpc21isp -control -wipe -bin firmware.bin \.\COM10
  115200 12000`) now reaches "Synchronizing (ESC to abort)..." — the
  board's ROM bootloader is now being polled at 115200 baud over the
  correct port.
- Full daemon-driven `fbuild deploy tests/platform/lpc845brk -e
  lpc845brk --port COM10` reaches the same sync phase and times out
  after 60 s because the board is not in ISP mode. Entering ISP mode
  (hold SW3, tap SW4, release SW3) is a hardware step tracked by
  #921 requirement 3 and is out of scope here.

Test additions (17/17 pass via real ~/.local/bin/soldr.exe):
- `normalize_port_prefixes_com10_and_above_on_windows` (Windows-only)
- `normalize_port_is_a_noop_on_non_windows` (POSIX-only)
@fastled-project-sync fastled-project-sync Bot moved this to Triage in FastLED Tracker Jul 2, 2026
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.

nxplpc: LpcDeployer feeds -hex to a .bin firmware and passes CMSIS-DAP speed=1000 as the lpc21isp baud

1 participant