diff --git a/marlin_host/transport.py b/marlin_host/transport.py index 8eeefdc..e97c705 100644 --- a/marlin_host/transport.py +++ b/marlin_host/transport.py @@ -135,7 +135,9 @@ def __init__( if reset_on_open: self.reset() - def reset(self, *, dtr: bool = True, rts: bool = False, assert_hold: float = 0.2) -> None: + def reset( + self, *, dtr: bool = True, rts: bool = False, assert_hold: float = 0.2, flush: bool = False + ) -> None: """Pulse the reset line so the controller reboots and re-emits ``start``. Follows the printrun sequence — assert DTR, hold ``assert_hold`` seconds, @@ -143,9 +145,16 @@ def reset(self, *, dtr: bool = True, rts: bool = False, assert_hold: float = 0.2 AVR/RAMPS and chip-fed 32-bit boards that edge couples to ``RESET`` through a small capacitor; native-USB boards have no such cap and ignore it (fine — ``MarlinHost.connect`` falls back to probing). RTS is left at the driver - default unless ``rts=True``; no standard Marlin board needs it. The input - buffer is deliberately not flushed so ``connect`` can match the ``start`` - greeting. ``assert_hold`` is the only board-specific knob here; see issue #4. + default unless ``rts=True``; no standard Marlin board needs it. + + By default the input buffer is **not** flushed, so ``connect`` can match the + ``start`` greeting on a normal open. Pass ``flush=True`` to discard buffered + input right after the reset edge — required when *recovering* a board that + already emitted stale output (e.g. ``Error:Printer halted. kill() called!`` + after an M112), which the next ``connect`` would otherwise consume and + misreport as a fresh halt. The post-reboot ``start`` arrives after the boot + delay, so it survives the flush. ``assert_hold`` is the board-specific + timing knob; see issue #4. """ if dtr: self._serial.dtr = True # assert -> adapter DTR pin low -> RESET edge @@ -156,6 +165,10 @@ def reset(self, *, dtr: bool = True, rts: bool = False, assert_hold: float = 0.2 self._serial.dtr = False # release if rts: self._serial.rts = False + if flush: + # Stale pre-reset output is already buffered; the reboot `start` is not + # yet, so dropping the buffer now clears the former and keeps the latter. + self._serial.reset_input_buffer() def write_line(self, line: str) -> None: self._serial.write((line + "\n").encode("ascii")) diff --git a/tests/test_serial_transport.py b/tests/test_serial_transport.py index 5ca99b1..ee8b07a 100644 --- a/tests/test_serial_transport.py +++ b/tests/test_serial_transport.py @@ -76,3 +76,29 @@ def close(self) -> None: sleep.assert_called_once() # held between assert and release t.close() assert ("close", None) in events + + +def test_reset_with_flush_discards_stale_input_after_the_edge() -> None: + """Recovery reset: flush stale pre-reset output (e.g. a post-M112 halt line) + only *after* the DTR release, so it can't poison the next connect.""" + events: list[tuple[str, object]] = [] + + class _RecordingSerial: + @property + def dtr(self) -> bool | None: + return None + + @dtr.setter + def dtr(self, value: bool) -> None: + events.append(("dtr", value)) + + def reset_input_buffer(self) -> None: + events.append(("flush", None)) + + rec = _RecordingSerial() + with patch("serial.serial_for_url", return_value=rec), patch("time.sleep"): + t = SerialTransport("loop://", reset_on_open=False) + t.reset(flush=True) + + # Flush happens last — after assert(True) and release(False), never before. + assert events == [("dtr", True), ("dtr", False), ("flush", None)]