Skip to content

Commit 49f46a6

Browse files
committed
tools/pyboard: Add automatic PTY device detection for QEMU.
Apply the same PTY detection fix to pyboard.py as mpremote (commit 5fb9bc1). The official test suite uses pyboard.py (tests/run-tests.py line 369), not mpremote. Both tools received the same breaking change in commit aaedd59 (May 2025), so both need the fix. This eliminates intermittent hangs when running tests against QEMU targets. Signed-off-by: Andrew Leech <[email protected]>
1 parent 5fb9bc1 commit 49f46a6

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)