Skip to content

Commit 917f2c5

Browse files
Improve instrumentation
1 parent 99613ca commit 917f2c5

2 files changed

Lines changed: 50 additions & 17 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,5 @@ MPOS Controller (`scripts/mpos_controller.py`):
7575
- IMPORTANT: `get_widget_tree()` and `get_visible_text()` include ALL children of scrollable parents, including off-screen items. y1/y2 coordinates are in content space, not screen space. To know what's actually visible, combine a screenshot (`mpos.screenshot()`) with the ppq-vision skill.
7676
- `_read_remote_file` / `write_remote_file`: ProcessBackend uses base64 (works over PTY), SerialBackend uses `mpremote cp` (reliable over USB).
7777
- `mpos.screenshot()` captures via `capture_screenshot()` on device, then reads raw file and converts to BMP via `_build_bmp()`.
78+
- The notification bar (top status bar) is NOT always present. It's controlled by the `bar_open` global in `internal_filesystem/lib/mpos/ui/topmenu.py`. Check it with `mpos.eval("mpos.ui.topmenu.bar_open")` (the module is already imported by `main.py`). When open, its height is `AppearanceManager.NOTIFICATION_BAR_HEIGHT` (24px).
7879
- All tests pass covering exec, eval, screenshot, input simulation, screen introspection, file I/O, and physical device control.

scripts/mpos_controller.py

Lines changed: 49 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import subprocess
2323
import sys
2424
import tempfile
25+
import termios
2526
import time
2627
import platform
2728

@@ -304,18 +305,23 @@ def read_until(self, ending, timeout=30):
304305

305306
def wait_for_boot(self, timeout=30):
306307
t0 = time.monotonic()
307-
self._drain(0.3)
308+
data = b""
309+
ctrl_c_sent = False
308310
while time.monotonic() - t0 < timeout:
309-
self.stream.write(b"\x03")
310-
time.sleep(0.2)
311-
if self._data_waiting(0.3):
312-
data = self.stream.read(4096)
313-
if b">>> " in data:
314-
self.stream.write(b"\n")
315-
time.sleep(0.3)
316-
self._drain(0.5)
317-
return
318-
# Ctrl-C didn't yield a prompt; try Ctrl-B to enter friendly REPL
311+
if self._data_waiting(0.1):
312+
chunk = self.stream.read(4096)
313+
if chunk:
314+
data += chunk
315+
if b">>> " in data:
316+
self._drain(0.5)
317+
self.stream.write(b"\n")
318+
time.sleep(0.2)
319+
self._drain(0.3)
320+
return
321+
elif not ctrl_c_sent and time.monotonic() - t0 > 2:
322+
self.stream.write(b"\x03")
323+
time.sleep(0.3)
324+
ctrl_c_sent = True
319325
self.stream.write(b"\x02")
320326
time.sleep(0.2)
321327
self._drain(0.3)
@@ -325,8 +331,8 @@ def wait_for_boot(self, timeout=30):
325331
"aioREPL prompt not found.\n" + data.decode("utf-8", "replace")[-2000:]
326332
)
327333
self.stream.write(b"\n")
328-
time.sleep(0.5)
329-
self._drain(1.0)
334+
time.sleep(0.3)
335+
self._drain(0.5)
330336

331337
def exec(self, code, timeout=30):
332338
"""
@@ -337,9 +343,12 @@ def exec(self, code, timeout=30):
337343
to delimit output from REPL chatter.
338344
"""
339345
self._drain(0.1)
346+
347+
# Enter paste mode and wait for it to engage
340348
self.stream.write(b"\x05")
341-
time.sleep(0.05)
342-
self._drain(0.1)
349+
time.sleep(0.2)
350+
self._drain(0.5)
351+
343352
payload = "print('{}')\n".format(SENTINEL) + code.rstrip()
344353
self.stream.write(payload.encode("utf-8"))
345354
self.stream.write(b"\x04")
@@ -388,6 +397,21 @@ def start(self):
388397
pass
389398
time.sleep(0.3)
390399
master_fd, slave_fd = pty.openpty()
400+
# Set master side to raw mode so control chars (Ctrl-C, Ctrl-D, Ctrl-E)
401+
# pass through unmodified instead of being intercepted by the line
402+
# discipline (which would consume Ctrl-D as EOF, break paste mode, etc.)
403+
try:
404+
iflag, oflag, cflag, lflag, ispeed, ospeed, cc = termios.tcgetattr(master_fd)
405+
iflag &= ~(termios.BRKINT | termios.IGNBRK | termios.ICRNL | termios.INLCR | termios.IGNCR | termios.IXON | termios.IXOFF | termios.PARMRK | termios.INPCK | termios.ISTRIP)
406+
oflag &= ~(termios.OPOST)
407+
cflag &= ~(termios.PARENB | termios.CSIZE)
408+
cflag |= termios.CS8
409+
lflag &= ~(termios.ECHO | termios.ECHONL | termios.ICANON | termios.ISIG | termios.IEXTEN)
410+
cc[termios.VMIN] = 1
411+
cc[termios.VTIME] = 0
412+
termios.tcsetattr(master_fd, termios.TCSANOW, [iflag, oflag, cflag, lflag, ispeed, ospeed, cc])
413+
except Exception:
414+
pass
391415
self.proc = subprocess.Popen(
392416
[
393417
self.binary,
@@ -419,10 +443,12 @@ def stop(self):
419443
if self.proc:
420444
try:
421445
os.killpg(os.getpgid(self.proc.pid), signal.SIGTERM)
422-
self.proc.wait(timeout=5)
446+
self.proc.wait(timeout=3)
423447
except Exception:
448+
pass
449+
if self.proc and self.proc.poll() is None:
450+
self.proc.kill()
424451
try:
425-
self.proc.kill()
426452
self.proc.wait(timeout=2)
427453
except Exception:
428454
pass
@@ -434,6 +460,12 @@ def stop(self):
434460
pass
435461
self.master_fd = None
436462

463+
def restart(self):
464+
"""Stop and restart the backend."""
465+
self.stop()
466+
time.sleep(0.3)
467+
self.start()
468+
437469
def __del__(self):
438470
self.stop()
439471

0 commit comments

Comments
 (0)