Skip to content

Commit 5fb9bc1

Browse files
committed
tools/mpremote: Add automatic PTY device detection for QEMU.
PTY devices used by QEMU don't reliably report inWaiting() status, causing intermittent hangs. While short tests often pass due to timing, long-running operations expose this issue. This adds automatic PTY detection (Linux /dev/pts/* with major 136) and uses blocking reads for PTY devices while maintaining non-blocking behavior for real serial devices. The fix respects the concern from commit 0d46e45 about indefinite blocking by relying on pyserial's existing timeout mechanism (interCharTimeout: 1) combined with read_until() timeout checks. Signed-off-by: Andrew Leech <[email protected]>
1 parent 4c11f64 commit 5fb9bc1

File tree

1 file changed

+46
-16
lines changed

1 file changed

+46
-16
lines changed

tools/mpremote/mpremote/transport_serial.py

Lines changed: 46 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
# Once the API is stabilised, the idea is that mpremote can be used both
3636
# as a command line tool and a library for interacting with devices.
3737

38-
import ast, io, os, re, struct, sys, time
38+
import ast, io, os, re, stat, struct, sys, time
3939
import serial
4040
import serial.tools.list_ports
4141
from errno import EPERM
@@ -105,6 +105,31 @@ def __init__(self, device, baudrate=115200, wait=0, exclusive=True, timeout=None
105105
if delayed:
106106
print("")
107107

108+
# Detect if this is a PTY device (e.g., QEMU serial output)
109+
# PTY devices don't reliably report inWaiting() status, so we need
110+
# to use blocking reads instead of checking for data availability.
111+
self.is_pty = self._is_pty_device(device)
112+
113+
def _is_pty_device(self, device):
114+
"""
115+
Detect if device is a PTY (pseudo-terminal).
116+
117+
PTY devices are commonly used by emulators like QEMU. Unlike real serial
118+
devices, PTY inWaiting() may not report data availability correctly,
119+
requiring use of blocking reads instead.
120+
"""
121+
try:
122+
# Linux Unix98 PTY pattern: /dev/pts/N
123+
if device.startswith("/dev/pts/"):
124+
st = os.stat(device)
125+
# Unix98 PTY slaves have major device number 136 on Linux
126+
if stat.S_ISCHR(st.st_mode) and os.major(st.st_rdev) == 136:
127+
return True
128+
except (OSError, AttributeError):
129+
# If detection fails or os.major not available, assume not a PTY
130+
pass
131+
return False
132+
108133
def close(self):
109134
# ESP Windows quirk: Prevent target from resetting when Windows clears DTR before RTS
110135
self.serial.rts = False
@@ -133,22 +158,27 @@ def read_until(
133158
while True:
134159
if data.endswith(ending):
135160
break
136-
elif self.serial.inWaiting() > 0:
161+
162+
# PTY: always read (blocking with timeout), Serial: check inWaiting() first
163+
if self.is_pty or self.serial.inWaiting() > 0:
137164
new_data = self.serial.read(1)
138-
if data_consumer:
139-
data_consumer(new_data)
140-
data = new_data
141-
else:
142-
data = data + new_data
143-
begin_char_s = time.monotonic()
144-
else:
145-
if timeout is not None and time.monotonic() >= begin_char_s + timeout:
146-
break
147-
if (
148-
timeout_overall is not None
149-
and time.monotonic() >= begin_overall_s + timeout_overall
150-
):
151-
break
165+
if new_data:
166+
if data_consumer:
167+
data_consumer(new_data)
168+
data = new_data
169+
else:
170+
data = data + new_data
171+
begin_char_s = time.monotonic()
172+
173+
# Check timeouts (applies to both PTY and real serial)
174+
if timeout is not None and time.monotonic() >= begin_char_s + timeout:
175+
break
176+
if (
177+
timeout_overall is not None
178+
and time.monotonic() >= begin_overall_s + timeout_overall
179+
):
180+
break
181+
if not self.is_pty:
152182
time.sleep(0.01)
153183
return data
154184

0 commit comments

Comments
 (0)