You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
LPC845-BRK: deploy must always unwedge the shared VCOM (pyocd/SWD flash leaves CDC in Windows error 31) #565
Flashing the LPC845-BRK via pyOCD/SWD wedges the board's shared USB VCOM on Windows: after a flash the CDC serial port goes to Device Manager error 31 ("This device cannot start") and only a physical USB replug brings it back. This breaks any flash-then-monitor flow (e.g. the FastLED AutoResearch RPC harness), which must reopen the serial port immediately after deploy.
Requirement: deploy must always unwedge — after every flash the VCOM must be usable again with no physical intervention.
Why this happens
The LPC845-BRK's on-board debug probe is a USB composite device exposing both a CMSIS-DAP HID interface and a CDC VCOM bridge (wired to P0_24/P0_25, per crates/fbuild-build/src/nxplpc/assets/arduino_stub/HardwareSerial.cpp). pyOCD opens the CMSIS-DAP HID to flash over SWD; its post-flash target reset leaves the composite device's CDC function wedged on Windows (error 31). Observed workaround so far is "don't issue a pyocd reset after flash" + physical replug.
Current state in fbuild
There is no LPC deployer wired yet. The daemon's platform dispatch falls through to:
crates/fbuild-daemon/src/handlers/operations/deploy.rs:707
"deployer for {:?} not yet implemented", // Platform::NxpLpc lands here
So today the board is flashed by pyOCD outside fbuild, with no recovery step — hence the manual replug. This is the deploy gap tracked by #551; this issue adds the "always unwedge" acceptance criterion to it.
Proposed fix
Implement an fbuild-deployDeployer for Platform::NxpLpc that owns the unwedge:
Prefer lpc21isp ISP-over-UART (the README's primary path, upload.protocol in lpc845brk.json). ISP flashing runs over the VCOM serial line itself and never opens the CMSIS-DAP HID, so the composite device cannot wedge. This is the cleanest "always unwedge."
If SWD/pyOCD is used, own a deterministic post-flash recovery instead of leaving it to pyocd's default:
Skip pyocd's post-flash reset (the step correlated with the wedge); rely on the baked-in vector checksum + a clean DAP disconnect to start the image.
If the VCOM is still wedged, force a Windows USB re-enumeration of the composite devnode (restart the device / cycle the port) before returning success — modeled on the existing TeensyDeployer state machine, which already "takes a wedged Windows host and recovers it" (crates/fbuild-deploy/src/teensy/).
Verify on hardware (LPC845-BRK): flash → reopen VCOM → AutoResearch RPC echo succeeds with no replug.
Acceptance
fbuild deploy <lpc845 project> -e lpc845brk flashes and the VCOM is immediately openable afterward, repeatably, with zero physical replugs.
/clud-fix burn-down — skipped (downstream of #551)
Touched as part of the #586 meta sweep. This issue is strictly downstream of #551 (no LPC Deployer exists yet, so there's nothing to attach the VCOM-unwedge logic to). Investigation report on #551 sketches the planned fix shape — including a dedicated vcom_unwedge.rs module specifically for the acceptance criterion this issue tracks. Will re-attempt this one automatically once #551 lands the LPC deployer scaffold.
Filed via Claude Code — /clud-fix skill burn-down pass.
Confirmed wedge on 2026-06-15 with refined repro + likely actual trigger
Reproduced today running bash autoresearch lpc845brk --pin-toggle-rx --upload-port COM10 on Windows 10 against the LPC845-BRK. The wedge is real and consistent. Adding more diagnostic detail because the probable trigger is different from what this issue's original body assumed -- and that changes the fix shape.
What FastLED's autoresearch actually does today
ci/autoresearch/phases.py:1684-1716 invokes two separate pyocd subprocesses back-to-back:
Each subprocess opens and closes the CMSIS-DAP HID interface independently. On the LPC845-BRK that interface is part of a composite USB device that also exposes the application VCOM CDC.
Observation today, after the merged work
I retested with the architected workarounds in place:
pyocd is using --no-reset on the load + reset_type=sw (NVIC SYSRESETREQ) on the separate commander -- not a hardware SRST, so the original "post-flash reset toggles the probe" hypothesis is ruled out for this exact code path.
The CMSIS-DAP probe firmware on the LPC4322 / on-board debugger is not observably reset by reset_type=sw (which is a target-side NVIC write, transparent to the probe).
And yet the CDC VCOM still wedges into Windows error 31 every single time. Reproducibly. The follow-up sequence is:
pyocd commander -O reset_type=sw -c reset -c go -c quit -- opens HID again, sends NVIC reset to the target, closes HID. (Subprocess cdc on boot warning #2 exits.)
Within ~50 ms of step 2, the Windows CDC sibling drops to error 31 and stays there until physical USB replug.
The fact that this still happens with --no-reset + reset_type=sw means the wedge trigger is not the reset itself. It is something about the second HID open-close cycle on a composite device that confuses Windows' CDC driver.
Refined root-cause hypothesis
The Windows usbccgp.sys (USB composite class generic parent driver) treats CMSIS-DAP HID + CDC as two function drivers under one device. When pyocd opens the HID, usbccgp serializes the HID interface. When pyocd closes it and then a different process re-opens it within a short window, usbccgp emits a function-level reset event on the device. That function-level reset doesn't affect the HID side (pyocd just re-handshakes) -- but the CDC sibling sees its endpoint cleared and the higher-level usbser.sys driver doesn't recover, going to error 31.
Two practical implications:
A single pyocd invocation that does load + reset would likely sidestep this. Either pyocd flash --reset (full unified command) or pyocd load -O reset_type=sw firmware.bin (single subprocess, default-on reset) keeps the HID open for the whole sequence.
The --no-reset + separate commander pattern is the active trigger, not the reset semantic. This is why reset_type=sw (which was added specifically for VCOM safety) doesn't save us -- it's not the reset that wedges the CDC, it's the inter-subprocess HID re-open.
Proposed fix for the autoresearch path (one-line in ci/autoresearch/phases.py)
Collapse the two subprocesses into one:
# Single subprocess: load + reset_type=sw all in one HID sessionload_cmd=pyocd_cmd_prefix+ [
"load", "--target", target,
"-O", "reset_type=sw", # was: --no-reset, then a 2nd commander callstr(firmware_bin),
]
result=subprocess.run(load_cmd, env=env)
# (no more 2nd `pyocd commander -c reset` step)
I'd expect this to drop the wedge incidence dramatically -- maybe entirely, maybe to "rare" depending on whether the reset itself also nudges the CDC. Worth testing.
If it still wedges after that, the architected answer is #595's lpc21isp path -- which sidesteps the composite HID entirely by going ISP-over-UART. (Currently lpc21isp isn't installed on this host, so I haven't been able to A/B that path empirically. The merged Rust LpcDeployer in fbuild#595 is the prerequisite.)
Acceptance criteria for closing this issue haven't shifted
The issue body's original requirement -- "deploy must always unwedge the shared VCOM" -- remains correct. The fix shape changes from "switch to SW reset" (already attempted, doesn't work) to one of:
A. Single-pyocd-call (cheapest; might resolve everything; test first)
Activity
zackees commentedon Jun 15, 2026
/clud-fix burn-down — skipped (downstream of #551)
Touched as part of the #586 meta sweep. This issue is strictly downstream of #551 (no LPC
Deployerexists yet, so there's nothing to attach the VCOM-unwedge logic to). Investigation report on #551 sketches the planned fix shape — including a dedicatedvcom_unwedge.rsmodule specifically for the acceptance criterion this issue tracks. Will re-attempt this one automatically once #551 lands the LPC deployer scaffold.Filed via Claude Code —
/clud-fixskill burn-down pass.feat(deploy/lpc): LpcDeployer Phase 1 — lpc21isp ISP-over-UART (closes …
zackees commentedon Jun 16, 2026
Confirmed wedge on 2026-06-15 with refined repro + likely actual trigger
Reproduced today running
bash autoresearch lpc845brk --pin-toggle-rx --upload-port COM10on Windows 10 against the LPC845-BRK. The wedge is real and consistent. Adding more diagnostic detail because the probable trigger is different from what this issue's original body assumed -- and that changes the fix shape.What FastLED's autoresearch actually does today
ci/autoresearch/phases.py:1684-1716invokes two separate pyocd subprocesses back-to-back:Each subprocess opens and closes the CMSIS-DAP HID interface independently. On the LPC845-BRK that interface is part of a composite USB device that also exposes the application VCOM CDC.
Observation today, after the merged work
I retested with the architected workarounds in place:
pyocd is using
--no-reseton the load +reset_type=sw(NVICSYSRESETREQ) on the separate commander -- not a hardware SRST, so the original "post-flash reset toggles the probe" hypothesis is ruled out for this exact code path.The CMSIS-DAP probe firmware on the LPC4322 / on-board debugger is not observably reset by
reset_type=sw(which is a target-side NVIC write, transparent to the probe).And yet the CDC VCOM still wedges into Windows error 31 every single time. Reproducibly. The follow-up sequence is:
pyocd load --no-reset-- opens HID, writes flash, closes HID. (Subprocess Esp32-s3 PSRAM use #1 exits.)pyocd commander -O reset_type=sw -c reset -c go -c quit-- opens HID again, sends NVIC reset to the target, closes HID. (Subprocess cdc on boot warning #2 exits.)The fact that this still happens with
--no-reset+reset_type=swmeans the wedge trigger is not the reset itself. It is something about the second HID open-close cycle on a composite device that confuses Windows' CDC driver.Refined root-cause hypothesis
The Windows usbccgp.sys (USB composite class generic parent driver) treats CMSIS-DAP HID + CDC as two function drivers under one device. When pyocd opens the HID, usbccgp serializes the HID interface. When pyocd closes it and then a different process re-opens it within a short window, usbccgp emits a function-level reset event on the device. That function-level reset doesn't affect the HID side (pyocd just re-handshakes) -- but the CDC sibling sees its endpoint cleared and the higher-level usbser.sys driver doesn't recover, going to error 31.
Two practical implications:
pyocd flash --reset(full unified command) orpyocd load -O reset_type=sw firmware.bin(single subprocess, default-on reset) keeps the HID open for the whole sequence.--no-reset+ separate commander pattern is the active trigger, not the reset semantic. This is whyreset_type=sw(which was added specifically for VCOM safety) doesn't save us -- it's not the reset that wedges the CDC, it's the inter-subprocess HID re-open.Proposed fix for the autoresearch path (one-line in
ci/autoresearch/phases.py)Collapse the two subprocesses into one:
I'd expect this to drop the wedge incidence dramatically -- maybe entirely, maybe to "rare" depending on whether the reset itself also nudges the CDC. Worth testing.
If it still wedges after that, the architected answer is #595's
lpc21isppath -- which sidesteps the composite HID entirely by going ISP-over-UART. (Currently lpc21isp isn't installed on this host, so I haven't been able to A/B that path empirically. The merged RustLpcDeployerin fbuild#595 is the prerequisite.)Acceptance criteria for closing this issue haven't shifted
The issue body's original requirement -- "deploy must always unwedge the shared VCOM" -- remains correct. The fix shape changes from "switch to SW reset" (already attempted, doesn't work) to one of:
lpc21ispISP-over-UART path (architected; requires the deployer wiring from feat(deploy/lpc): LpcDeployer Phase 1 — lpc21isp ISP-over-UART (closes #551, sidesteps #565) #595 and alpc21ispinstall)winusbIOCTL ordevconinvocation)I lean A first (try the unified
pyocd load -O reset_type=sw), B as the architected long-term answer, and C if neither A nor B holds 100% of the time.Cross-references from today's bench session
Happy to test the A fix locally and report back.
Filed via Claude Code on behalf of zachvorhies@protonmail.com.
ci(hooks): forbidden_commands.py — block pyocd/esptool/dfu-util/picot…
feat(deploy): resolve lpc21isp path via env / cache / tools / PATH (#921
feat(deploy): lpc21isp path resolver + baud/hex safety nets (#921, #927…
refactor(autoresearch): delegate all deployment to fbuild (removes py…