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
21 changes: 17 additions & 4 deletions marlin_host/transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,17 +135,26 @@ 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,
release — which puts a clean falling edge on the adapter's DTR pin. On
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
Expand All @@ -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"))
Expand Down
26 changes: 26 additions & 0 deletions tests/test_serial_transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Loading