diff --git a/crates/fbuild-cli/src/cli/serial_probe.rs b/crates/fbuild-cli/src/cli/serial_probe.rs index a107050a8..89e14ec59 100644 --- a/crates/fbuild-cli/src/cli/serial_probe.rs +++ b/crates/fbuild-cli/src/cli/serial_probe.rs @@ -232,12 +232,14 @@ fn read_port(port_name: &str, baud: u32, seconds: f64, send: Option<&str>) -> Re .timeout(Duration::from_millis(100)) .open() .map_err(|e| FbuildError::SerialError(format!("open `{port_name}` @ {baud}: {e}")))?; - handle - .write_data_terminal_ready(dtr) - .map_err(|e| FbuildError::SerialError(format!("set DTR={dtr} on `{port_name}`: {e}")))?; + // RTS before DTR: clearing DTR first passes through (DTR=0, RTS=1), + // which resets ESP USB-Serial-JTAG chips (FastLED/fbuild#1617). handle .write_request_to_send(rts) .map_err(|e| FbuildError::SerialError(format!("set RTS={rts} on `{port_name}`: {e}")))?; + handle + .write_data_terminal_ready(dtr) + .map_err(|e| FbuildError::SerialError(format!("set DTR={dtr} on `{port_name}`: {e}")))?; output::progress(format!( "probe read: port={port_name} baud={baud} family={family:?} \ diff --git a/crates/fbuild-serial/src/esp_reset.rs b/crates/fbuild-serial/src/esp_reset.rs index 75407b6c7..63d5578b3 100644 --- a/crates/fbuild-serial/src/esp_reset.rs +++ b/crates/fbuild-serial/src/esp_reset.rs @@ -77,6 +77,25 @@ impl DtrRtsControl for T { } } +/// Drive a freshly opened port to its idle `(dtr, rts)` state. +/// +/// The OS asserts DTR=1, RTS=1 on open. RTS is written first: clearing +/// DTR first would pass through `(DTR=0, RTS=1)`, which the ESP +/// USB-Serial-JTAG reads as "hold the chip in reset" and releases into a +/// reboot (on the ESP32-P4 a full USB re-enumeration). Going via +/// `(DTR=1, RTS=0)` only sets the boot strap (FastLED/fbuild#1617). +/// Failures are logged and ignored — some adapters reject control-line +/// writes but the port is still usable. +pub fn apply_idle_control_lines(port: &mut P, dtr: bool, rts: bool) { + if let Err(e) = port.write_request_to_send(rts) { + tracing::warn!("failed to set RTS={rts}: {e}"); + } + if let Err(e) = port.write_data_terminal_ready(dtr) { + tracing::warn!("failed to set DTR={dtr}: {e}"); + } + tracing::debug!("open-time idle lines applied: RTS={rts} then DTR={dtr}"); +} + /// Hold time for the RTS=high (EN=low) pulse, in milliseconds. /// /// Matches `esptool`'s classic-hardware `hard_reset` timing: long enough to @@ -250,6 +269,23 @@ mod tests { } } + #[test] + fn idle_lines_clear_rts_before_dtr() { + let mut port = RecordedPort::default(); + apply_idle_control_lines(&mut port, false, false); + assert_eq!(port.events, vec![("RTS", false), ("DTR", false)]); + } + + #[test] + fn idle_lines_still_set_dtr_when_rts_write_fails() { + let mut port = RecordedPort { + fail_on: Some("rts"), + ..RecordedPort::default() + }; + apply_idle_control_lines(&mut port, true, true); + assert_eq!(port.events, vec![("DTR", true)]); + } + #[test] fn esp_hard_reset_emits_canonical_sequence() { let mut port = RecordedPort::default(); diff --git a/crates/fbuild-serial/src/manager.rs b/crates/fbuild-serial/src/manager.rs index a53286eee..585d29413 100644 --- a/crates/fbuild-serial/src/manager.rs +++ b/crates/fbuild-serial/src/manager.rs @@ -248,20 +248,7 @@ impl SharedSerialManager { let (dtr, rts) = family_for_open .map(|f| f.idle_dtr_rts()) .unwrap_or((true, true)); - match serial.write_data_terminal_ready(dtr) { - Ok(()) => tracing::debug!( - family = ?family_for_open, - "manager: open-time DTR={dtr} asserted" - ), - Err(e) => tracing::warn!("failed to set DTR={dtr}: {}", e), - } - match serial.write_request_to_send(rts) { - Ok(()) => tracing::debug!( - family = ?family_for_open, - "manager: open-time RTS={rts} asserted" - ), - Err(e) => tracing::warn!("failed to set RTS={rts}: {}", e), - } + crate::esp_reset::apply_idle_control_lines(serial.as_mut(), dtr, rts); Ok(serial) }) .await; @@ -1214,20 +1201,7 @@ impl SharedSerialManager { let mut serial = serialport::new(&port_for_open, baud_rate) .timeout(SERIAL_READ_TIMEOUT) .open()?; - match serial.write_data_terminal_ready(dtr) { - Ok(()) => tracing::debug!( - family = ?family_for_open, - "manager: open-time DTR={dtr} asserted" - ), - Err(e) => tracing::warn!("failed to set DTR: {}", e), - } - match serial.write_request_to_send(rts) { - Ok(()) => tracing::debug!( - family = ?family_for_open, - "manager: open-time RTS={rts} asserted" - ), - Err(e) => tracing::warn!("failed to set RTS: {}", e), - } + crate::esp_reset::apply_idle_control_lines(serial.as_mut(), dtr, rts); Ok(serial) }) .await;