Skip to content

Commit 87f7025

Browse files
committed
tools: Add PTY device detection for QEMU serial support.
Improve mpremote and pyboard.py serial connection handling to detect and properly configure PTY devices. This enables running tests via QEMU's serial PTY output. Changes to transport_serial.py and pyboard.py to check if the serial device is a PTY and adjust timeout/configuration accordingly. Signed-off-by: Andrew Leech <[email protected]>
1 parent e17b33e commit 87f7025

File tree

2 files changed

+102
-31
lines changed

2 files changed

+102
-31
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

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)