Skip to content

Latest commit

 

History

History
217 lines (184 loc) · 10.2 KB

File metadata and controls

217 lines (184 loc) · 10.2 KB

Deploy architecture

How firmware actually gets from fbuild deploy to a connected board. The point of this doc is so an agent adding a new board family knows which pieces to extend and which ones to leave alone, instead of copy-pasting an ESP-shaped path onto a CDC-bridge board and losing an afternoon to the FastLED/FastLED#3300 trap.

High-level flow

fbuild-cli (`deploy` subcommand)
    │   thin HTTP client — no build/flash logic lives here
    ▼
fbuild-daemon (HTTP/WebSocket server)
    │   request validation, device lease, build orchestration
    ▼
fbuild-build::BuildOrchestrator (per-platform impl)
    │   produces firmware.elf, firmware.bin, build_info.json
    ▼
fbuild-deploy::Deployer trait
    │   board-family-specific flash path
    │     • ESP32: esptool / espflash via serial
    │     • LPC8xx: pyOCD / probe-rs via CMSIS-DAP USB
    │     • Teensy / SAMD / RP2040: 1200-bps touch + UF2 copy
    ▼
post_deploy_recovery (Deployer-supplied)
    │   serial-port re-enumeration wait, board-specific quirks
    ▼
fbuild-serial::manager (re-attach monitor)
    │   open_port with DTR=true, RTS=true per docs/usb-cdc-control-line-matrix.md
    ▼
Client receives stream over WebSocket

The Deployer trait

crates/fbuild-deploy/src/lib.rs::Deployer is the contract every board family implements:

trait Deployer {
    fn deploy(&self, ...) -> Result<()>;
    fn post_deploy_recovery(&self, port: &str) -> Result<()>;  // default: 3s sleep
}

post_deploy_recovery is the cross-family hook for "the board's USB endpoint just disappeared because we reset it; here's how long / how to wait for it to come back." Override when:

  • The board uses USB-bootloader flashing (Teensy / RP2040 / SAMD) — the bootloader's VID:PID is different from the app's. The recovery has to poll for the app's VID:PID to reappear.
  • The board uses a CMSIS-DAP debug probe (LPC845-BRK) — the debug probe stays enumerated through reset; the VCOM bridge may blink on Windows. Need a board-specific wait.
  • The board is LPC + CMSIS-DAP specifically — wedge recovery needs extra retries; see #605 Phase 1.

Board-family dispatch

BoardFamily (in crates/fbuild-serial/src/boards.rs) is the taxonomy that gates DTR/RTS conventions today. It will eventually also gate the Deployer selection (#687 — the polymorphic ResetMethod dispatch registry) and per-family handoff timing (#691).

Today the dispatch is more ad-hoc:

  • ESP variants → fbuild-deploy::esp32_native (espflash) or fbuild-deploy::esp32_external_uart (esptool over UART)
  • LPC8xx → fbuild-deploy::pyocd_cmsis_dap (placeholder; bring-up in progress in the LPC845-BRK meta, #586)
  • Teensy / SAMD / RP2040 → 1200-bps-touch path (still partial)

The follow-up issues track the full polymorphic version:

  • #687 — BoardFamily enum + polymorphic ResetMethod dispatch registry. The thing this doc will reference as the canonical source.
  • #688 — BootModeClassifier registry (ESP-only today; generalize to per-family).
  • #691 — HandoffTiming on BoardFamily.
  • #692 — enumerate supported deploy protocols per board; fail fast on unsupported.
  • #693 — USB-level bootloader re-enumeration detection (complement to #688).

RP2040/RP2350 deploy tunables

crates/fbuild-deploy/src/rp2040.rs (--transport picotool|uf2, #1162) selects which stock transport is tried first:

  • Shared reset ladder. After the pre-touch scan and 1200-bps CDC touch, fbuild waits for BOOTSEL. If none appears and the selected runtime endpoint supplied an exact VID, PID, and non-empty USB serial, it asks the Pico SDK application reset interface to enter BOOTSEL and waits again. On Windows, fbuild maps the selected CDC identity to one exact healthy WinUSB reset interface and sends the Pico class control request directly. Other hosts use managed picotool reboot -u --vid ... --pid ... -f; picotool derives the runtime serial from the opened application device because that interface cannot be selected reliably with --ser. The fallback is attempted only after fbuild resolves one exact runtime identity, and picotool refuses a forced command when the VID/PID is ambiguous. A missing or ambiguous identity skips this layer, so an unscoped forced command is impossible. Deployment results name the application reset-interface reboot when it ran successfully.
  • picotool (default). After the shared reset ladder and UF2 preparation, fbuild derives one exact BOOTSEL VID:PID from the verified FastLED/boards profile for the selected RP family and binds each picotool operation to that identity plus the selected runtime USB serial: --vid 0x<registry-vid> --pid 0x<registry-pid> --ser <serial>. It then runs a Windows-only PICOBOOT driver preflight for that same composite interface, a bounded picotool info probe, and picotool load <uf2> -x. The ROM load does not use -f; forced application reset is the separately scoped ladder step above. A missing runtime serial or ambiguous/missing registry BOOTSEL identity disables picotool; fbuild uses only an explicitly identified BOOTSEL mass-storage volume. A Windows driver problem, including Code 43, also skips picotool rather than spending its timeout. Any remaining picotool failure falls back to the BOOTSEL mass-storage path; if that also fails, the combined error names both transports' failures.
  • uf2. Preserves the historical order exactly: BOOTSEL mass-storage first (with bounded transfer retries across fresh enumerations), managed picotool as the fallback.

Each stage-timeout env var accepts integer seconds in 1..=600; anything else logs a warning and keeps the default:

Env var Default Governs
FBUILD_RP2040_BOOTLOADER_TIMEOUT_SECS 10 s BOOTSEL volume discovery after the 1200-bps touch (and re-discovery between transfer retries)
FBUILD_RP2040_UF2_WRITE_TIMEOUT_SECS 60 s Per-attempt watchdog on the NEW.UF2 write; a timed-out write feeds the normal retry/picotool-fallback path
FBUILD_RP2040_POST_DEPLOY_TIMEOUT_SECS 15 s Eject watch after the write, and the runtime-CDC reappearance wait
FBUILD_RP2040_PICOTOOL_TIMEOUT_SECS 60 s Target-bound picotool load -x budget, both as the picotool-primary load and the picotool-fallback load

Outcome note: once the eject watch (mass-storage path) or a successful picotool load (primary or fallback) has confirmed the ROM accepted the image, a quiet runtime-CDC window no longer fails the deploy — it reports success with no port ("flashed, CDC unconfirmed"), so CI does not re-flash a healthy board whose first-plug driver install outlived the window. A genuine port-enumeration error still fails.

On Windows, the daemon retains the last non-empty LocationPaths observed for a runtime endpoint. An unidentified VID_0000&PID_0002 Code 43 node is eligible for the explicit/admin-gated exact-child restart only when one and only one matching RP history has the same normalized physical USB path. The elevated helper re-queries that path before acting. A different location or multiple matches fail closed; fbuild never cycles a hub or changes the host-wide selective-suspend policy.

Worked example — "agent ports a new RP2350 board"

The right sequence today:

  1. VID:PID first. Publish the board's bootloader and runtime USB endpoints in FastLED/boards, then exercise fbuild's normal catalogue ingestion path. Never add a literal VID/PID to BOARD_FINGERPRINTS, ENVIRONMENT_TO_VCOM, generated Rust, or any other production fallback in this repository.
  2. Family classification. Resolve the published FastLED/boards product identity and map that metadata to CdcAcmBridge (or the appropriate family behavior). If the catalogue cannot express a needed distinction, extend its published schema instead of embedding another ID table in fbuild. RP2350 normally uses 1200-bps touch and host-ready idle, matching the RP2040 convention.
  3. DTR/RTS matrix. Add a row to docs/usb-cdc-control-line-matrix.md for the new chip, with a datasheet citation and capture date.
  4. Deployer impl. If the existing fbuild-deploy::pyocd_cmsis_dap or RP2040 1200-bps-touch path is reusable, register the new board there. Otherwise add a new sibling module under crates/fbuild-deploy/src/<family>/. Implement Deployer + override post_deploy_recovery if the recovery timing differs.
  5. Test. fbuild serial probe list should now show the new board's hint. fbuild serial probe find --env <new-env> should return the right port. fbuild deploy --env <new-env> should complete; --monitor should reattach without dropping bytes.

Do NOT start by copy-pasting an esp32_native deploy path. The default ESP DTR/RTS state is (false, false) — fatal for any CDC-ACM bridge.

Worked example — "agent debugs deploy failure on COM20"

fbuild deploy -e lpc845brk
> error: pyocd CMSIS-DAP connect failed: device not found

The deploy path goes through the CMSIS-DAP USB endpoint ((0x1FC9, 0x0132)), NOT the COM port. Two separate USB devices.

# Check what's actually enumerated
$ fbuild serial probe list
COM20      16C0:0483  ser=…  [LPC11U35 VCOM bridge (LPC845-BRK USART0) OR PJRC Teensy USB-Serial]
COM10      1FC9:0132  ser=…  [NXP CMSIS-DAP debug (LPC845-BRK / LPC11U35)]

Both endpoints present — good. pyOCD's failure is its own (probably a driver / udev rule issue, not an fbuild bug). The point is: COM20 is the data port, COM10 is the deploy port. Don't try to flash via the wrong endpoint.

See also

Filed in #695.