Skip to content

Commit 1304134

Browse files
committed
tools/pyboard: Add automatic PTY device detection for QEMU.
Applies the same PTY detection fix to pyboard.py as mpremote. The official test suite uses pyboard.py (tests/run-tests.py), so both tools need the fix. See PR micropython#18327 for detailed analysis and validation. Signed-off-by: Andrew Leech <[email protected]>
1 parent fec4d12 commit 1304134

File tree

1 file changed

+54
-15
lines changed

1 file changed

+54
-15
lines changed

tools/pyboard.py

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
import ast
7171
import errno
7272
import os
73+
import stat
7374
import struct
7475
import sys
7576
import time
@@ -333,6 +334,39 @@ def __init__(
333334
if delayed:
334335
print("")
335336

337+
# Detect if this is a PTY device (e.g., QEMU serial output)
338+
# PTY devices don't reliably report inWaiting() status, so we need
339+
# to use blocking reads instead of checking for data availability.
340+
# Only check for actual serial devices, not exec/telnet connections.
341+
if not (
342+
device.startswith("exec:")
343+
or device.startswith("execpty:")
344+
or (device and device[0].isdigit() and device[-1].isdigit() and device.count(".") == 3)
345+
):
346+
self.is_pty = self._is_pty_device(device)
347+
else:
348+
self.is_pty = False
349+
350+
def _is_pty_device(self, device):
351+
"""
352+
Detect if device is a PTY (pseudo-terminal).
353+
354+
PTY devices are commonly used by emulators like QEMU. Unlike real serial
355+
devices, PTY inWaiting() may not report data availability correctly,
356+
requiring use of blocking reads instead.
357+
"""
358+
try:
359+
# Linux Unix98 PTY pattern: /dev/pts/N
360+
if device.startswith("/dev/pts/"):
361+
st = os.stat(device)
362+
# Unix98 PTY slaves have major device number 136 on Linux
363+
if stat.S_ISCHR(st.st_mode) and os.major(st.st_rdev) == 136:
364+
return True
365+
except (OSError, AttributeError):
366+
# If detection fails or os.major not available, assume not a PTY
367+
pass
368+
return False
369+
336370
def close(self):
337371
self.serial.close()
338372

@@ -358,22 +392,27 @@ def read_until(
358392
while True:
359393
if data.endswith(ending):
360394
break
361-
elif self.serial.inWaiting() > 0:
395+
396+
# PTY: always read (blocking with timeout), Serial: check inWaiting() first
397+
if self.is_pty or self.serial.inWaiting() > 0:
362398
new_data = self.serial.read(1)
363-
if data_consumer:
364-
data_consumer(new_data)
365-
data = new_data
366-
else:
367-
data = data + new_data
368-
begin_char_s = time.monotonic()
369-
else:
370-
if timeout is not None and time.monotonic() >= begin_char_s + timeout:
371-
break
372-
if (
373-
timeout_overall is not None
374-
and time.monotonic() >= begin_overall_s + timeout_overall
375-
):
376-
break
399+
if new_data:
400+
if data_consumer:
401+
data_consumer(new_data)
402+
data = new_data
403+
else:
404+
data = data + new_data
405+
begin_char_s = time.monotonic()
406+
407+
# Check timeouts (applies to both PTY and real serial)
408+
if timeout is not None and time.monotonic() >= begin_char_s + timeout:
409+
break
410+
if (
411+
timeout_overall is not None
412+
and time.monotonic() >= begin_overall_s + timeout_overall
413+
):
414+
break
415+
if not self.is_pty:
377416
time.sleep(0.01)
378417
return data
379418

0 commit comments

Comments
 (0)