Skip to content

Commit fec4d12

Browse files
committed
tools/mpremote: Add automatic PTY device detection for QEMU.
PTY devices used by QEMU don't reliably report inWaiting() status. This adds automatic PTY detection (Linux /dev/pts/* with major 136) and uses blocking reads for PTYs while maintaining non-blocking behavior for real serial devices. Fixes intermittent hangs when running tests against QEMU targets. See PR micropython#18327 for detailed analysis and validation. Signed-off-by: Andrew Leech <[email protected]>
1 parent 27b7bf3 commit fec4d12

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, ENOTTY
@@ -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
try:
@@ -140,22 +165,27 @@ def read_until(
140165
while True:
141166
if data.endswith(ending):
142167
break
143-
elif self.serial.inWaiting() > 0:
168+
169+
# PTY: always read (blocking with timeout), Serial: check inWaiting() first
170+
if self.is_pty or self.serial.inWaiting() > 0:
144171
new_data = self.serial.read(1)
145-
if data_consumer:
146-
data_consumer(new_data)
147-
data = new_data
148-
else:
149-
data = data + new_data
150-
begin_char_s = time.monotonic()
151-
else:
152-
if timeout is not None and time.monotonic() >= begin_char_s + timeout:
153-
break
154-
if (
155-
timeout_overall is not None
156-
and time.monotonic() >= begin_overall_s + timeout_overall
157-
):
158-
break
172+
if new_data:
173+
if data_consumer:
174+
data_consumer(new_data)
175+
data = new_data
176+
else:
177+
data = data + new_data
178+
begin_char_s = time.monotonic()
179+
180+
# Check timeouts (applies to both PTY and real serial)
181+
if timeout is not None and time.monotonic() >= begin_char_s + timeout:
182+
break
183+
if (
184+
timeout_overall is not None
185+
and time.monotonic() >= begin_overall_s + timeout_overall
186+
):
187+
break
188+
if not self.is_pty:
159189
time.sleep(0.01)
160190
return data
161191

0 commit comments

Comments
 (0)