Skip to content

Commit eac20b7

Browse files
committed
improved monitor, auto exit
1 parent 57721e0 commit eac20b7

6 files changed

Lines changed: 354 additions & 32 deletions

File tree

README.md

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,11 @@ RESET soft
240240
$ mpytool reset --machine # MCU reset (machine.reset, auto-reconnect)
241241
RESET machine
242242
243-
$ mpytool reset -- monitor # reset and monitor output
243+
$ mpytool reset -- monitor # reset and monitor until program ends
244+
RESET soft
245+
MONITOR (until program ends, -f to follow)
246+
247+
$ mpytool reset -- monitor -f # reset and monitor continuously
244248
RESET soft
245249
MONITOR (Ctrl+C to stop)
246250
@@ -261,7 +265,8 @@ REPL (Ctrl+] to exit) CWD: / PATH: : :.frozen :/lib
261265
262266
$ mpytool -p /dev/ttyUSB0 repl # specify port
263267
$ mpytool -b 9600 repl # specify baudrate
264-
$ mpytool -p /dev/ttyUSB0 -b 9600 monitor # monitor at 9600 baud
268+
$ mpytool -p /dev/ttyUSB0 -b 9600 monitor # monitor until program ends
269+
$ mpytool monitor -f # follow output continuously (Ctrl-C to stop)
265270
```
266271

267272
The `repl` command with `-v` flag displays current working directory (CWD) and `sys.path` before entering REPL, making it easy to see the device state after `mount`, `cd`, or `path` commands.
@@ -270,24 +275,30 @@ Both `repl` and `monitor` can be used as general-purpose serial tools - not just
270275

271276
### Execute Python code on device
272277
```
273-
$ mpytool exec "print('Hello!')"
278+
$ mpytool exec "print('Hello!')" # execute and stream output
274279
EXEC: print('Hello!')
275280
Hello!
276281
277282
$ mpytool exec "import sys; print(sys.version)"
278283
EXEC: import sys; print(sys.version)
279284
3.4.0; MicroPython v1.24.0
285+
286+
$ mpytool exec "long_task()" -t 30 # timeout after 30 seconds
287+
$ mpytool exec "start_server()" -t 0 # fire-and-forget (don't wait)
280288
```
281289

282290
### Run local Python file on device
283291
```
284-
$ mpytool run script.py # run script (fire-and-forget)
292+
$ mpytool run script.py # run and stream output
293+
RUN: script.py (128 bytes)
294+
Hello from script!
295+
296+
$ mpytool run script.py -t 0 # fire-and-forget (don't wait)
285297
RUN: script.py (128 bytes)
286298
287-
$ mpytool run script.py -- monitor # run script and capture output
299+
$ mpytool run script.py -t 10 # timeout after 10 seconds
288300
RUN: script.py (128 bytes)
289-
MONITOR (Ctrl+C to stop)
290-
Hello from script!
301+
Done!
291302
```
292303

293304
### Edit file on device

README_API.md

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1045,20 +1045,24 @@ Low-level REPL communication. Usually accessed via `mpy.comm`.
10451045

10461046
### Methods
10471047

1048-
#### exec(command, timeout=5)
1048+
#### exec(command, timeout=None, stream=False)
10491049

10501050
Execute Python command on device.
10511051

10521052
```python
1053-
mpy.comm.exec(command, timeout=5)
1053+
mpy.comm.exec(command, timeout=None, stream=False)
10541054
```
10551055

10561056
**Parameters:**
10571057
- `command` (str): Python code to execute
1058-
- `timeout` (int): Maximum wait time in seconds. `0` = submit only (send code, don't wait for output)
1058+
- `timeout` (float|None): Total wall time in seconds. `None` = wait forever, `0` = submit only (fire-and-forget)
1059+
- `stream`: Output mode:
1060+
- `False` (default): Return all output as `bytes`
1061+
- `True`: Return a generator yielding `bytes` chunks
1062+
- file-like object: Write chunks to it (supports both binary and text streams via `io.TextIOBase` detection), return `b''`
10591063

10601064
**Returns:**
1061-
- `bytes`: Command stdout output (`b''` when timeout=0)
1065+
- `bytes`, generator, or `b''` (when stream is file-like or timeout=0)
10621066

10631067
**Example:**
10641068
```python
@@ -1068,6 +1072,15 @@ b'Hello\r\n'
10681072
>>> mpy.comm.exec("import sys")
10691073
b''
10701074

1075+
# Streaming output (generator)
1076+
>>> for chunk in mpy.comm.exec("print('Hello')", stream=True):
1077+
... print(chunk)
1078+
1079+
# Stream to stdout
1080+
>>> import sys
1081+
>>> mpy.comm.exec("print('Hello')", stream=sys.stdout.buffer)
1082+
b''
1083+
10711084
# Submit code without waiting for output (fire-and-forget)
10721085
>>> mpy.comm.exec("while True: print('tick')", timeout=0)
10731086
b''
@@ -1132,23 +1145,24 @@ mpy.comm.enter_raw_repl()
11321145
mpy.comm.exit_raw_repl()
11331146
```
11341147

1135-
#### exec_raw_paste(command, timeout=5)
1148+
#### exec_raw_paste(command, timeout=5, stream=False)
11361149

11371150
Execute Python command using raw-paste mode with flow control.
11381151

11391152
Raw-paste mode compiles code as it receives it, using less RAM and providing
11401153
better reliability for large code transfers. Requires MicroPython 1.17+.
11411154

11421155
```python
1143-
mpy.comm.exec_raw_paste(command, timeout=5)
1156+
mpy.comm.exec_raw_paste(command, timeout=5, stream=False)
11441157
```
11451158

11461159
**Parameters:**
11471160
- `command` (str or bytes): Python code to execute
1148-
- `timeout` (int): Maximum wait time in seconds. `0` = submit only (send code, don't wait for output)
1161+
- `timeout` (float|None): Total wall time in seconds. `0` = submit only (fire-and-forget)
1162+
- `stream`: Output mode (same as `exec()`: `False`/`True`/file-like object)
11491163

11501164
**Returns:**
1151-
- `bytes`: Command stdout output (`b''` when timeout=0)
1165+
- `bytes`, generator, or `b''` (depending on `stream` and `timeout`)
11521166

11531167
**Example:**
11541168
```python
@@ -1165,20 +1179,21 @@ b'99\r\n'
11651179
- `MpyError`: If raw-paste mode is not supported by device
11661180
- `CmdError`: If command raises an exception on device
11671181

1168-
#### try_raw_paste(command, timeout=5)
1182+
#### try_raw_paste(command, timeout=5, stream=False)
11691183

11701184
Try raw-paste mode, fall back to regular exec if not supported.
11711185

11721186
```python
1173-
mpy.comm.try_raw_paste(command, timeout=5)
1187+
mpy.comm.try_raw_paste(command, timeout=5, stream=False)
11741188
```
11751189

11761190
**Parameters:**
11771191
- `command` (str or bytes): Python code to execute
1178-
- `timeout` (int): Maximum wait time in seconds. `0` = submit only (send code, don't wait for output)
1192+
- `timeout` (float|None): Total wall time in seconds. `0` = submit only (fire-and-forget)
1193+
- `stream`: Output mode (same as `exec()`: `False`/`True`/file-like object)
11791194

11801195
**Returns:**
1181-
- `bytes`: Command stdout output (`b''` when timeout=0)
1196+
- `bytes`, generator, or `b''` (depending on `stream` and `timeout`)
11821197

11831198
**Example:**
11841199
```python
@@ -1191,6 +1206,37 @@ This method automatically detects if raw-paste mode is supported and caches
11911206
the result. On older MicroPython versions, it silently falls back to regular
11921207
`exec()`.
11931208

1209+
#### monitor(stream=False, follow=False)
1210+
1211+
Monitor device output in normal REPL mode.
1212+
1213+
By default, reads output until REPL prompt (`>>>`) is detected (program finished).
1214+
With `follow=True`, reads indefinitely (Ctrl-C to stop).
1215+
1216+
```python
1217+
mpy.comm.monitor(stream=False, follow=False)
1218+
```
1219+
1220+
**Parameters:**
1221+
- `stream`: Output mode (same as `exec()`: `False`/`True`/file-like object)
1222+
- `follow` (bool): If `True`, don't stop at REPL prompt
1223+
1224+
**Returns:**
1225+
- `bytes`, generator, or `b''` (depending on `stream`)
1226+
1227+
**Example:**
1228+
```python
1229+
# Capture output until program ends
1230+
>>> output = mpy.comm.monitor()
1231+
1232+
# Stream to stdout
1233+
>>> import sys
1234+
>>> mpy.comm.monitor(stream=sys.stdout.buffer)
1235+
1236+
# Follow continuously
1237+
>>> mpy.comm.monitor(stream=sys.stdout.buffer, follow=True)
1238+
```
1239+
11941240
## Exception Classes
11951241

11961242
### MpyError

mpytool/mount.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@
179179
_ESCAPE_BYTE = bytes([ESCAPE])
180180

181181
_SOFT_REBOOT = b'soft reboot'
182-
_REPL_PROMPT = b'>>> '
182+
from mpytool.mpy_comm import REPL_PROMPT as _REPL_PROMPT
183183
_REBOOT_BUF_MAX = 256
184184
_REBOOT_BUF_KEEP = 64
185185
_LISTDIR_LIMIT = 1000 # max entries returned by CMD_LISTDIR

mpytool/mpy_comm.py

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""MicroPython tool: MPY communication"""
22

3+
import io as _io
34
import re as _re
45
import time as _time
56

@@ -13,6 +14,10 @@
1314
CTRL_D = b'\x04' # Execute / Soft reset / End raw-paste
1415
CTRL_E = b'\x05' # Paste mode
1516

17+
# REPL prompt
18+
REPL_PROMPT = b'>>> '
19+
REPL_PROMPT_LINE = b'\r\n' + REPL_PROMPT
20+
1621
# Raw-paste mode
1722
RAW_PASTE_ENTER = CTRL_E + b'A' + CTRL_A # Enter raw-paste mode sequence
1823
RAW_PASTE_ACK = b'\x01' # Flow control ACK
@@ -121,7 +126,7 @@ def stop_current_operation(self):
121126
if attempt >= 4:
122127
self._conn.write(b'\x18')
123128
try:
124-
self._conn.read_until(b'\r\n>>> ', timeout=0.2)
129+
self._conn.read_until(REPL_PROMPT_LINE, timeout=0.2)
125130
self._repl_mode = False
126131
return True
127132
except _conn.Timeout:
@@ -149,7 +154,7 @@ def exit_raw_repl(self):
149154
return
150155
self._log.info('EXIT RAW REPL')
151156
self._conn.write(CTRL_B)
152-
self._conn.read_until(b'\r\n>>> ')
157+
self._conn.read_until(REPL_PROMPT_LINE)
153158
self._repl_mode = False
154159

155160
def soft_reset(self):
@@ -176,6 +181,52 @@ def reset_state(self):
176181
self._repl_mode = None
177182
self._raw_paste_supported = None
178183

184+
def monitor(self, stream=False, follow=False):
185+
"""Monitor device output in normal REPL mode.
186+
187+
Reads device output until REPL prompt (>>>) is detected,
188+
or indefinitely if follow=True.
189+
190+
Arguments:
191+
stream: False = return bytes, True = yield chunks,
192+
file-like object = write chunks to it and return b''
193+
follow: if True, don't stop at REPL prompt
194+
195+
Returns:
196+
bytes result, generator, or b'' (when stream is file-like)
197+
"""
198+
self.exit_raw_repl()
199+
if stream is True:
200+
return self._monitor_chunks(follow)
201+
if stream:
202+
return self._monitor_to(stream, follow)
203+
return b''.join(self._monitor_chunks(follow))
204+
205+
def _monitor_chunks(self, follow):
206+
"""Yield output chunks until REPL prompt (or forever if follow)."""
207+
prompt_len = len(REPL_PROMPT_LINE)
208+
tail = b''
209+
while True:
210+
data = self._conn.read(timeout=0.1)
211+
if data is None:
212+
continue
213+
yield data
214+
if not follow:
215+
# Keep enough bytes so prompt split across chunks is found
216+
tail = tail[-(prompt_len - 1):] + data
217+
if REPL_PROMPT_LINE in tail:
218+
return
219+
220+
def _monitor_to(self, out, follow):
221+
"""Write monitor output to file-like object."""
222+
text_mode = isinstance(out, _io.TextIOBase)
223+
for chunk in self._monitor_chunks(follow):
224+
out.write(
225+
chunk.decode('utf-8', 'backslashreplace')
226+
if text_mode else chunk)
227+
out.flush()
228+
return b''
229+
179230
def exec(self, command, timeout=None, stream=False):
180231
"""Execute command
181232
@@ -333,7 +384,6 @@ def _exec_stream_to(self, command, timeout, out):
333384
334385
Supports both binary and text streams (io.TextIOBase).
335386
"""
336-
import io as _io
337387
start_time = _time.time()
338388
result = bytearray()
339389
text_mode = isinstance(out, _io.TextIOBase)

mpytool/mpytool.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -438,13 +438,14 @@ def cmd_rm(self, *file_names):
438438
self.verbose(f"RM: {path}", 1)
439439
self.mpy.delete(path)
440440

441-
def cmd_monitor(self):
442-
self.verbose("MONITOR (Ctrl+C to stop)", 1)
441+
def cmd_monitor(self, follow=False):
442+
if follow:
443+
self.verbose("MONITOR (Ctrl+C to stop)", 1)
444+
else:
445+
self.verbose("MONITOR (until program ends, -f to follow)", 1)
446+
out = getattr(_sys.stdout, 'buffer', _sys.stdout)
443447
try:
444-
while True:
445-
line = self.conn.read_line()
446-
line = line.decode('utf-8', 'backslashreplace')
447-
print(line)
448+
self.mpy.comm.monitor(stream=out, follow=follow)
448449
except KeyboardInterrupt:
449450
self.verbose('', level=0, overwrite=True) # newline after ^C
450451
except (_mpytool.ConnError, OSError) as err:
@@ -907,11 +908,13 @@ def _dispatch_reset(self, commands, is_last_group):
907908
reconnect = has_more if mode in ('machine', 'rts') else True
908909
self.cmd_reset(mode=mode, reconnect=reconnect, timeout=args.timeout)
909910

910-
@command('monitor', 'Monitor device output. Press Ctrl-C to stop.')
911+
@command('monitor', 'Monitor device output until program ends.')
912+
@option('-f', '--follow', action='store_true',
913+
help='follow output continuously (Ctrl-C to stop)')
911914
def _dispatch_monitor(self, commands, is_last_group):
912-
_, commands[:] = _make_parser(
915+
args, commands[:] = _make_parser(
913916
self._dispatch_monitor).parse_known_args(commands)
914-
self.cmd_monitor()
917+
self.cmd_monitor(follow=args.follow)
915918

916919
@command('repl', 'Interactive REPL session. Press Ctrl-] to exit.')
917920
def _dispatch_repl(self, commands, is_last_group):

0 commit comments

Comments
 (0)