Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
161 changes: 161 additions & 0 deletions crates/fbuild-serial/src/boards.rs
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,112 @@ impl BoardFamily {
pub fn reset_is_serial_native(&self) -> bool {
matches!(self.reset_method(), ResetMethod::DtrRtsPulse)
}

/// Per-family flash → monitor handoff timing.
///
/// FastLED/fbuild#691. Numbers track FastLED/FastLED#3339's LPC
/// bring-up plus observed values from earlier ESP + RP2040 +
/// Teensy + Arduino sessions. See
/// `docs/usb-cdc-control-line-matrix.md` (#689) for the per-row
/// table and citations.
///
/// Consumed by `Deployer::post_deploy_recovery` (#605) instead of
/// per-deployer inline magic numbers.
///
/// # Examples
///
/// ```
/// use fbuild_serial::boards::BoardFamily;
///
/// let esp = BoardFamily::Esp32NativeUsbCdc.handoff_timing();
/// assert_eq!(esp.post_reset_settle_ms, 200);
/// assert_eq!(esp.boot_drain_ms, 0);
///
/// let lpc = BoardFamily::CdcAcmBridge.handoff_timing();
/// assert_eq!(lpc.post_reset_settle_ms, 500);
/// assert_eq!(lpc.boot_drain_ms, 2000); // LPC11U35 bridge needs the drain
/// ```
#[must_use]
pub fn handoff_timing(&self) -> HandoffTiming {
use BoardFamily::*;
match self {
// ESP32 native + external UART: short settle, no drain
// (peripheral elides pre-app garbage), 5 retries through
// CDC re-enum.
Esp32NativeUsbCdc | Esp32ExternalUart => HandoffTiming {
post_reset_settle_ms: 200,
boot_drain_ms: 0,
port_reappear_timeout_ms: 3000,
open_retry_count: 5,
},
// CDC-ACM bridge (LPC11U35): pyOCD reset settles in ~500
// ms but the bridge re-emits ~2 s of boot-banner garbage
// that must be drained before the bring-up RPC. From
// FastLED/FastLED#3339.
CdcAcmBridge => HandoffTiming {
post_reset_settle_ms: 500,
boot_drain_ms: 2000,
port_reappear_timeout_ms: 3000,
open_retry_count: 3,
},
// 1200-bps-touch bootloaders (Teensy HalfKay, SAMD UF2,
// RP2040 BOOTSEL): the port DROPS for ~1-2 s then
// reappears at the *bootloader* VID/PID, then drops
// again and reappears at the app VID/PID after flash.
// Tolerate up to 5 s reappear + 10 open retries to ride
// out the double-enumeration window.
Teensy | NativeUsbCdcReset1200Bps => HandoffTiming {
post_reset_settle_ms: 100,
boot_drain_ms: 500,
port_reappear_timeout_ms: 5000,
open_retry_count: 10,
},
// Arduino auto-reset: the bootloader's "wait for upload"
// window is ~1.5 s — must sleep through it before reading
// app output. Port doesn't drop (USB endpoint stays on
// the bridge chip, not the AVR), so reappear timeout is
// 0 and only 1 open is needed.
ArduinoAutoReset => HandoffTiming {
post_reset_settle_ms: 1500,
boot_drain_ms: 0,
port_reappear_timeout_ms: 0,
open_retry_count: 1,
},
}
}
}

/// Flash → monitor handoff timing for a board family.
///
/// FastLED/fbuild#691 — concrete numbers from the FastLED/FastLED#3339
/// LPC845-BRK bring-up incident + observed values across the ESP /
/// Teensy / RP2040 / Arduino ecosystems. Used by
/// `Deployer::post_deploy_recovery` (FastLED/fbuild#605) instead of
/// per-deployer inline magic numbers.
///
/// All fields are `u32` milliseconds; consume via
/// `Duration::from_millis(timing.field)`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HandoffTiming {
/// How long to sleep after `reset` returns before the first byte
/// is expected on the serial port. Covers the device's boot-ROM
/// → app-firmware-entry latency.
pub post_reset_settle_ms: u32,
/// How long to drain residual boot-banner / boot-up junk before
/// the bring-up RPC sends its first request. Bridges (LPC11U35)
/// and stretched-boot devices benefit; ESP native USB is
/// zero-drain because the CDC peripheral itself elides the
/// pre-app garbage.
pub boot_drain_ms: u32,
/// How long to wait for the port to reappear after the USB endpoint
/// drops (CDC re-enum, 1200-bps-touch bootloader, BOOTSEL). Set
/// to `0` for boards that don't drop the port at all (Arduino
/// classic auto-reset).
pub port_reappear_timeout_ms: u32,
/// Max retries on transient open failures during the reappear
/// window. Higher for boards with longer re-enumeration windows
/// (Teensy / RP2040 — HalfKay / BOOTSEL).
pub open_retry_count: u8,
}

/// The hardware primitive that resets a board.
Expand Down Expand Up @@ -575,6 +681,61 @@ mod tests {
}
}

// ─── FastLED/fbuild#691: HandoffTiming per-family table ───────

#[test]
fn handoff_timing_matches_fastled_3339_lpc_numbers() {
// The whole point of #691 is that the LPC845-BRK numbers from
// FastLED/FastLED#3339 don't drift in three places. Pin them.
let t = BoardFamily::CdcAcmBridge.handoff_timing();
assert_eq!(t.post_reset_settle_ms, 500);
assert_eq!(t.boot_drain_ms, 2000);
assert_eq!(t.port_reappear_timeout_ms, 3000);
assert_eq!(t.open_retry_count, 3);
}

#[test]
fn handoff_timing_esp_families_short_settle_no_drain() {
for family in [
BoardFamily::Esp32NativeUsbCdc,
BoardFamily::Esp32ExternalUart,
] {
let t = family.handoff_timing();
assert_eq!(t.post_reset_settle_ms, 200);
assert_eq!(t.boot_drain_ms, 0, "ESP CDC peripheral elides boot garbage");
}
}

#[test]
fn handoff_timing_1200bps_families_tolerate_double_enum() {
for family in [BoardFamily::Teensy, BoardFamily::NativeUsbCdcReset1200Bps] {
let t = family.handoff_timing();
// 1200-bps-touch bootloaders enumerate TWICE — bootloader
// VID/PID, then app VID/PID after flash. Tolerate both.
assert!(
t.port_reappear_timeout_ms >= 3000,
"family {family:?} needs ≥3 s reappear window"
);
assert!(
t.open_retry_count >= 5,
"family {family:?} needs ≥5 retries through double-enum"
);
}
}

#[test]
fn handoff_timing_arduino_long_settle_no_reappear() {
let t = BoardFamily::ArduinoAutoReset.handoff_timing();
assert!(
t.post_reset_settle_ms >= 1000,
"Arduino bootloader 'wait for upload' window is ~1.5 s"
);
assert_eq!(
t.port_reappear_timeout_ms, 0,
"Arduino USB endpoint stays on the bridge chip; no reappear"
);
}

#[test]
fn new_variants_appear_in_family_for_vid_pid_classification() {
// RP2040 → NativeUsbCdcReset1200Bps
Expand Down
13 changes: 13 additions & 0 deletions docs/usb-cdc-control-line-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,19 @@ data and which triggers reset — would let a new board's row be
filled in empirically without datasheet archaeology. Mentioned in
[FastLED/fbuild#686]'s discussion; not yet implemented.

## Flash → monitor handoff timing (FastLED/fbuild#691)

| BoardFamily | `post_reset_settle_ms` | `boot_drain_ms` | `port_reappear_timeout_ms` | `open_retry_count` |
|---|---:|---:|---:|---:|
| `Esp32NativeUsbCdc` | 200 | 0 | 3000 | 5 |
| `Esp32ExternalUart` | 200 | 0 | 3000 | 5 |
| `CdcAcmBridge` (LPC11U35) | 500 | 2000 | 3000 | 3 |
| `Teensy` | 100 | 500 | 5000 | 10 |
| `NativeUsbCdcReset1200Bps` (RP2040/SAMD) | 100 | 500 | 5000 | 10 |
| `ArduinoAutoReset` | 1500 | 0 | 0 | 1 |

Source: `crates/fbuild-serial/src/boards.rs::BoardFamily::handoff_timing`. The LPC11U35 row is from FastLED/FastLED#3339 (the bring-up incident). 1200-bps-touch rows tolerate the double-enumeration window (bootloader VID/PID then app VID/PID). Arduino has zero `port_reappear_timeout_ms` because the USB endpoint lives on the bridge chip and never drops.

## When to update this doc

- A new board family lands in `crates/fbuild-build/src/<family>/`.
Expand Down
Loading