Skip to content

Commit 510f259

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 510f259

File tree

1 file changed

+56
-15
lines changed

1 file changed

+56
-15
lines changed

tools/pyboard.py

Lines changed: 56 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,41 @@ 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+
if device.startswith("execpty:"):
341+
# execpty: explicitly uses PTY devices
342+
self.is_pty = True
343+
elif device.startswith("exec:") or (
344+
device and device[0].isdigit() and device[-1].isdigit() and device.count(".") == 3
345+
):
346+
# exec: (non-PTY) and telnet connections are not PTYs
347+
self.is_pty = False
348+
else:
349+
# For direct serial device paths, auto-detect
350+
self.is_pty = self._is_pty_device(device)
351+
352+
def _is_pty_device(self, device):
353+
"""
354+
Detect if device is a PTY (pseudo-terminal).
355+
356+
PTY devices are commonly used by emulators like QEMU. Unlike real serial
357+
devices, PTY inWaiting() may not report data availability correctly,
358+
requiring use of blocking reads instead.
359+
"""
360+
try:
361+
# Linux Unix98 PTY pattern: /dev/pts/N
362+
if device.startswith("/dev/pts/"):
363+
st = os.stat(device)
364+
# Unix98 PTY slaves have major device number 136 on Linux
365+
if stat.S_ISCHR(st.st_mode) and os.major(st.st_rdev) == 136:
366+
return True
367+
except (OSError, AttributeError):
368+
# If detection fails or os.major not available, assume not a PTY
369+
pass
370+
return False
371+
336372
def close(self):
337373
self.serial.close()
338374

@@ -358,22 +394,27 @@ def read_until(
358394
while True:
359395
if data.endswith(ending):
360396
break
361-
elif self.serial.inWaiting() > 0:
397+
398+
# PTY: always read (blocking with timeout), Serial: check inWaiting() first
399+
if self.is_pty or self.serial.inWaiting() > 0:
362400
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
401+
if new_data:
402+
if data_consumer:
403+
data_consumer(new_data)
404+
data = new_data
405+
else:
406+
data = data + new_data
407+
begin_char_s = time.monotonic()
408+
409+
# Check timeouts (applies to both PTY and real serial)
410+
if timeout is not None and time.monotonic() >= begin_char_s + timeout:
411+
break
412+
if (
413+
timeout_overall is not None
414+
and time.monotonic() >= begin_overall_s + timeout_overall
415+
):
416+
break
417+
if not self.is_pty:
377418
time.sleep(0.01)
378419
return data
379420

0 commit comments

Comments
 (0)