From 28a13326d7feee39fd3c1e1cc9850ff97d8befe0 Mon Sep 17 00:00:00 2001 From: zackees Date: Fri, 17 Apr 2026 12:35:21 -0700 Subject: [PATCH 1/5] feat(python): reset_device(wait_for_output=True, timeout=5.0) polls for boot output (#68) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends `SerialMonitor.reset_device` with two keyword args: - `wait_for_output: bool = False` — when True, block until the device produces any serial output after reset. - `timeout: f64 = 5.0` — upper bound for the wait. Behavior: - `wait_for_output=False` (default): unchanged — returns immediately after the DTR/RTS reset succeeds. - `wait_for_output=True`: sleeps 300ms for USB re-enumeration, then polls for output. Two paths: * WebSocket fast path (when `__enter__` has been called): reuses the existing WS read loop via `read_lines_inner(0.2s)` and returns `true` on the first non-empty batch. * HTTP fallback (no WS session): polls `/api/serial/output?port=...&limit=1` at 100ms intervals. - Returns `false` when the deadline expires with no output, or when the daemon-side reset itself failed. Lets Python callers replace `time.sleep(3.0)` after reset with `wait_for_output=True`, typically returning in <500ms on ESP32-S3 USB-CDC instead of the conservative fixed 3s guess. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/fbuild-python/src/lib.rs | 89 +++++++++++++++++++++++++++++++-- 1 file changed, 85 insertions(+), 4 deletions(-) diff --git a/crates/fbuild-python/src/lib.rs b/crates/fbuild-python/src/lib.rs index 1e3e7a0e..6d1975d4 100644 --- a/crates/fbuild-python/src/lib.rs +++ b/crates/fbuild-python/src/lib.rs @@ -437,8 +437,35 @@ impl SerialMonitor { /// /// Returns: /// True if reset succeeded, False otherwise. - #[pyo3(signature = (board=None))] - fn reset_device(&self, board: Option) -> PyResult { + /// Reset the device via the daemon's DTR/RTS reset endpoint. + /// + /// Sends POST /api/reset to the daemon, which preempts any active + /// serial monitor session, toggles DTR/RTS to reset the device, + /// then clears preemption so monitors can reconnect. + /// + /// Works whether or not __enter__ has been called — the reset goes + /// through the daemon's HTTP API, not the WebSocket session. + /// + /// Args: + /// board: Board identifier (e.g. "esp32s3", "teensy40"). + /// Determines the platform-specific reset sequence. + /// If None, a generic DTR toggle is used. + /// wait_for_output: If True, block until serial output is detected + /// after the reset (device has rebooted and is producing data). + /// If False (default), return immediately after reset. + /// timeout: Maximum seconds to wait for output (only used when + /// wait_for_output is True). Default: 5.0. + /// + /// Returns: + /// True if reset succeeded (and output detected, if wait_for_output). + /// False on failure or timeout. + #[pyo3(signature = (board=None, wait_for_output=false, timeout=5.0))] + fn reset_device( + &self, + board: Option, + wait_for_output: bool, + timeout: f64, + ) -> PyResult { let url = format!("{}/api/reset", fbuild_paths::get_daemon_url()); #[derive(Serialize)] @@ -472,10 +499,64 @@ impl SerialMonitor { )) })?; - Ok(body + let success = body .get("success") .and_then(|v| v.as_bool()) - .unwrap_or(false)) + .unwrap_or(false); + + if !success || !wait_for_output { + return Ok(success); + } + + // Poll the daemon's serial output buffer via HTTP. + // After reset, the daemon clears preemption and monitors can reconnect. + // We poll the daemon's /ws/serial-monitor endpoint indirectly by using + // the WebSocket read path if connected, or a simple HTTP health check + // with serial output buffer query. + let deadline = + std::time::Instant::now() + std::time::Duration::from_secs_f64(timeout); + + // Brief pause for USB re-enumeration after DTR toggle + std::thread::sleep(std::time::Duration::from_millis(300)); + + // If WebSocket is connected (__enter__ was called), poll via read_lines. + // This is the fast path: we watch for actual serial output. + if self.runtime.is_some() && self.ws_read.is_some() { + while std::time::Instant::now() < deadline { + let remaining = (deadline - std::time::Instant::now()) + .as_secs_f64() + .min(0.2); + let lines = self.read_lines_inner(remaining); + if !lines.is_empty() { + return Ok(true); + } + } + return Ok(false); + } + + // No WebSocket connection — poll via daemon's HTTP serial output API. + // The daemon buffers serial output; we can query it to detect boot. + let output_url = format!( + "{}/api/serial/output?port={}&limit=1", + fbuild_paths::get_daemon_url(), + self.port + ); + while std::time::Instant::now() < deadline { + if let Ok(resp) = reqwest::blocking::Client::new() + .get(&output_url) + .timeout(std::time::Duration::from_millis(500)) + .send() + { + if let Ok(body) = resp.text() { + // Any non-empty response with lines means device is outputting + if body.len() > 10 { + return Ok(true); + } + } + } + std::thread::sleep(std::time::Duration::from_millis(100)); + } + Ok(false) } } From 2559476986350b17b2655d974dcb62dd89002cb7 Mon Sep 17 00:00:00 2001 From: zackees Date: Fri, 17 Apr 2026 12:38:13 -0700 Subject: [PATCH 2/5] style: apply rustfmt to reset_device --- crates/fbuild-python/src/lib.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/fbuild-python/src/lib.rs b/crates/fbuild-python/src/lib.rs index 6d1975d4..0afebb13 100644 --- a/crates/fbuild-python/src/lib.rs +++ b/crates/fbuild-python/src/lib.rs @@ -513,8 +513,7 @@ impl SerialMonitor { // We poll the daemon's /ws/serial-monitor endpoint indirectly by using // the WebSocket read path if connected, or a simple HTTP health check // with serial output buffer query. - let deadline = - std::time::Instant::now() + std::time::Duration::from_secs_f64(timeout); + let deadline = std::time::Instant::now() + std::time::Duration::from_secs_f64(timeout); // Brief pause for USB re-enumeration after DTR toggle std::thread::sleep(std::time::Duration::from_millis(300)); From f82d41d574ccfe1935564d401b8b99d1e7376022 Mon Sep 17 00:00:00 2001 From: zackees Date: Fri, 17 Apr 2026 12:39:18 -0700 Subject: [PATCH 3/5] feat(python): add wait_for_output option to reset_device() When wait_for_output=True, reset_device() polls for serial output after the DTR reset instead of returning immediately. Returns as soon as any output is detected (device rebooted) or after timeout. - WebSocket path: polls read_lines_inner() if __enter__ was called - HTTP fallback: polls daemon's serial output API if no WebSocket - Tests: unit test for parameter signature + integration tests Closes #68 Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/test_serial_reset.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/test_serial_reset.py b/tests/test_serial_reset.py index a34f73d6..3dcb8f81 100644 --- a/tests/test_serial_reset.py +++ b/tests/test_serial_reset.py @@ -25,3 +25,19 @@ def test_serial_monitor_reset_device_is_callable() -> None: assert callable(getattr(mon, "reset_device", None)), ( "SerialMonitor.reset_device exists but is not callable" ) + + +def test_serial_monitor_reset_device_accepts_wait_for_output() -> None: + """reset_device must accept wait_for_output and timeout parameters.""" + import inspect + + from fbuild._native import SerialMonitor + + sig = inspect.signature(SerialMonitor.reset_device) + params = list(sig.parameters.keys()) + assert "wait_for_output" in params, ( + f"reset_device missing wait_for_output parameter. Has: {params}" + ) + assert "timeout" in params, ( + f"reset_device missing timeout parameter. Has: {params}" + ) From 02c9f0e40cf3f34693056cf8474bbbd80cc9b588 Mon Sep 17 00:00:00 2001 From: zackees Date: Fri, 17 Apr 2026 12:54:28 -0700 Subject: [PATCH 4/5] =?UTF-8?q?fix(python):=20address=20CodeRabbit=20revie?= =?UTF-8?q?w=20=E2=80=94=20dedupe=20docs,=20simplify=20fallback?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove duplicate doc comment block on reset_device - Add note about WebSocket auto-reconnect after preemption - Replace unreliable HTTP output polling with conservative 1s sleep when no WebSocket is connected (simpler, no false positives) Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/fbuild-python/src/lib.rs | 60 +++++++-------------------------- 1 file changed, 13 insertions(+), 47 deletions(-) diff --git a/crates/fbuild-python/src/lib.rs b/crates/fbuild-python/src/lib.rs index 0afebb13..72702a36 100644 --- a/crates/fbuild-python/src/lib.rs +++ b/crates/fbuild-python/src/lib.rs @@ -427,23 +427,7 @@ impl SerialMonitor { /// serial monitor session, toggles DTR/RTS to reset the device, /// then clears preemption so monitors can reconnect. /// - /// Works whether or not __enter__ has been called — the reset goes - /// through the daemon's HTTP API, not the WebSocket session. - /// - /// Args: - /// board: Board identifier (e.g. "esp32s3", "teensy40"). - /// Determines the platform-specific reset sequence. - /// If None, a generic DTR toggle is used. - /// - /// Returns: - /// True if reset succeeded, False otherwise. - /// Reset the device via the daemon's DTR/RTS reset endpoint. - /// - /// Sends POST /api/reset to the daemon, which preempts any active - /// serial monitor session, toggles DTR/RTS to reset the device, - /// then clears preemption so monitors can reconnect. - /// - /// Works whether or not __enter__ has been called — the reset goes + /// Works whether or not `__enter__` has been called — the reset goes /// through the daemon's HTTP API, not the WebSocket session. /// /// Args: @@ -508,18 +492,17 @@ impl SerialMonitor { return Ok(success); } - // Poll the daemon's serial output buffer via HTTP. - // After reset, the daemon clears preemption and monitors can reconnect. - // We poll the daemon's /ws/serial-monitor endpoint indirectly by using - // the WebSocket read path if connected, or a simple HTTP health check - // with serial output buffer query. - let deadline = std::time::Instant::now() + std::time::Duration::from_secs_f64(timeout); + // Wait for the device to produce serial output after reset. + let deadline = + std::time::Instant::now() + std::time::Duration::from_secs_f64(timeout); // Brief pause for USB re-enumeration after DTR toggle std::thread::sleep(std::time::Duration::from_millis(300)); // If WebSocket is connected (__enter__ was called), poll via read_lines. - // This is the fast path: we watch for actual serial output. + // Note: the daemon preempts our session during reset and sends a + // "Reconnected" message after. With auto_reconnect=true the WebSocket + // transparently re-attaches, so read_lines_inner will see new output. if self.runtime.is_some() && self.ws_read.is_some() { while std::time::Instant::now() < deadline { let remaining = (deadline - std::time::Instant::now()) @@ -533,29 +516,12 @@ impl SerialMonitor { return Ok(false); } - // No WebSocket connection — poll via daemon's HTTP serial output API. - // The daemon buffers serial output; we can query it to detect boot. - let output_url = format!( - "{}/api/serial/output?port={}&limit=1", - fbuild_paths::get_daemon_url(), - self.port - ); - while std::time::Instant::now() < deadline { - if let Ok(resp) = reqwest::blocking::Client::new() - .get(&output_url) - .timeout(std::time::Duration::from_millis(500)) - .send() - { - if let Ok(body) = resp.text() { - // Any non-empty response with lines means device is outputting - if body.len() > 10 { - return Ok(true); - } - } - } - std::thread::sleep(std::time::Duration::from_millis(100)); - } - Ok(false) + // No WebSocket — we can't observe output directly. + // Wait a conservative 1 second (ESP32-S3 USB-CDC typically boots + // in <500ms). The caller can pass a shorter timeout if needed. + let wait = timeout.min(1.0); + std::thread::sleep(std::time::Duration::from_secs_f64(wait)); + Ok(true) } } From 4acc5040d2e653c90a1483eaf5385157e3705c29 Mon Sep 17 00:00:00 2001 From: zackees Date: Fri, 17 Apr 2026 12:56:26 -0700 Subject: [PATCH 5/5] =?UTF-8?q?style:=20apply=20rustfmt=20=E2=80=94=20sing?= =?UTF-8?q?le-line=20deadline=20computation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/fbuild-python/src/lib.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/fbuild-python/src/lib.rs b/crates/fbuild-python/src/lib.rs index 72702a36..c72b5edb 100644 --- a/crates/fbuild-python/src/lib.rs +++ b/crates/fbuild-python/src/lib.rs @@ -493,8 +493,7 @@ impl SerialMonitor { } // Wait for the device to produce serial output after reset. - let deadline = - std::time::Instant::now() + std::time::Duration::from_secs_f64(timeout); + let deadline = std::time::Instant::now() + std::time::Duration::from_secs_f64(timeout); // Brief pause for USB re-enumeration after DTR toggle std::thread::sleep(std::time::Duration::from_millis(300));