mpytool 2.3.3__tar.gz → 2.3.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. {mpytool-2.3.3/mpytool.egg-info → mpytool-2.3.4}/PKG-INFO +27 -16
  2. mpytool-2.3.3/PKG-INFO → mpytool-2.3.4/README.md +24 -27
  3. {mpytool-2.3.3 → mpytool-2.3.4}/mpytool/__init__.py +1 -1
  4. {mpytool-2.3.3 → mpytool-2.3.4}/mpytool/conn_serial.py +3 -6
  5. {mpytool-2.3.3 → mpytool-2.3.4}/mpytool/mount.py +1 -1
  6. {mpytool-2.3.3 → mpytool-2.3.4}/mpytool/mpy_comm.py +135 -11
  7. {mpytool-2.3.3 → mpytool-2.3.4}/mpytool/mpytool.py +80 -28
  8. {mpytool-2.3.3 → mpytool-2.3.4}/mpytool/utils.py +78 -8
  9. mpytool-2.3.3/README.md → mpytool-2.3.4/mpytool.egg-info/PKG-INFO +38 -13
  10. {mpytool-2.3.3 → mpytool-2.3.4}/pyproject.toml +3 -3
  11. {mpytool-2.3.3 → mpytool-2.3.4}/tests/test_mpy.py +355 -0
  12. {mpytool-2.3.3 → mpytool-2.3.4}/tests/test_mpytool.py +69 -27
  13. {mpytool-2.3.3 → mpytool-2.3.4}/tests/test_utils.py +96 -0
  14. {mpytool-2.3.3 → mpytool-2.3.4}/LICENSE +0 -0
  15. {mpytool-2.3.3 → mpytool-2.3.4}/mpytool/cmd_cp.py +0 -0
  16. {mpytool-2.3.3 → mpytool-2.3.4}/mpytool/conn.py +0 -0
  17. {mpytool-2.3.3 → mpytool-2.3.4}/mpytool/conn_socket.py +0 -0
  18. {mpytool-2.3.3 → mpytool-2.3.4}/mpytool/logger.py +0 -0
  19. {mpytool-2.3.3 → mpytool-2.3.4}/mpytool/mpy.py +0 -0
  20. {mpytool-2.3.3 → mpytool-2.3.4}/mpytool/mpy_cross.py +0 -0
  21. {mpytool-2.3.3 → mpytool-2.3.4}/mpytool/speedtest.py +0 -0
  22. {mpytool-2.3.3 → mpytool-2.3.4}/mpytool/terminal.py +0 -0
  23. {mpytool-2.3.3 → mpytool-2.3.4}/mpytool/terminal_unix.py +0 -0
  24. {mpytool-2.3.3 → mpytool-2.3.4}/mpytool/terminal_win.py +0 -0
  25. {mpytool-2.3.3 → mpytool-2.3.4}/mpytool.egg-info/SOURCES.txt +0 -0
  26. {mpytool-2.3.3 → mpytool-2.3.4}/mpytool.egg-info/dependency_links.txt +0 -0
  27. {mpytool-2.3.3 → mpytool-2.3.4}/mpytool.egg-info/entry_points.txt +0 -0
  28. {mpytool-2.3.3 → mpytool-2.3.4}/mpytool.egg-info/requires.txt +0 -0
  29. {mpytool-2.3.3 → mpytool-2.3.4}/mpytool.egg-info/top_level.txt +0 -0
  30. {mpytool-2.3.3 → mpytool-2.3.4}/setup.cfg +0 -0
  31. {mpytool-2.3.3 → mpytool-2.3.4}/tests/test_cli.py +0 -0
  32. {mpytool-2.3.3 → mpytool-2.3.4}/tests/test_cmd_cp.py +0 -0
  33. {mpytool-2.3.3 → mpytool-2.3.4}/tests/test_errors.py +0 -0
  34. {mpytool-2.3.3 → mpytool-2.3.4}/tests/test_integration.py +0 -0
  35. {mpytool-2.3.3 → mpytool-2.3.4}/tests/test_mount.py +0 -0
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mpytool
3
- Version: 2.3.3
3
+ Version: 2.3.4
4
4
  Summary: MPY tool - manage files on devices running MicroPython
5
- Author-email: Pavel Revak <pavel.revak@gmail.com>
5
+ Author-email: Pavel Revak <pavelrevak@gmail.com>
6
6
  License-Expression: MIT
7
- Project-URL: Homepage, https://github.com/pavelrevak/mpytool
7
+ Project-URL: Homepage, https://github.com/cortexm/mpytool
8
8
  Keywords: MPY,micropython
9
9
  Requires-Python: >=3.10
10
10
  Description-Content-Type: text/markdown
@@ -55,7 +55,7 @@ pip3 install mpytool
55
55
  ### Installation from git (latest development version)
56
56
 
57
57
  ```bash
58
- pip3 install git+https://github.com/pavelrevak/mpytool.git
58
+ pip3 install git+https://github.com/cortexm/mpytool.git
59
59
  ```
60
60
 
61
61
  ### Installation in virtualenv
@@ -254,7 +254,11 @@ RESET soft
254
254
  $ mpytool reset --machine # MCU reset (machine.reset, auto-reconnect)
255
255
  RESET machine
256
256
 
257
- $ mpytool reset -- monitor # reset and monitor output
257
+ $ mpytool reset -- monitor # reset and monitor until program ends
258
+ RESET soft
259
+ MONITOR (until program ends, -f to follow)
260
+
261
+ $ mpytool reset -- monitor -f # reset and monitor continuously
258
262
  RESET soft
259
263
  MONITOR (Ctrl+C to stop)
260
264
 
@@ -275,7 +279,8 @@ REPL (Ctrl+] to exit) CWD: / PATH: : :.frozen :/lib
275
279
 
276
280
  $ mpytool -p /dev/ttyUSB0 repl # specify port
277
281
  $ mpytool -b 9600 repl # specify baudrate
278
- $ mpytool -p /dev/ttyUSB0 -b 9600 monitor # monitor at 9600 baud
282
+ $ mpytool -p /dev/ttyUSB0 -b 9600 monitor # monitor until program ends
283
+ $ mpytool monitor -f # follow output continuously (Ctrl-C to stop)
279
284
  ```
280
285
 
281
286
  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.
@@ -284,24 +289,30 @@ Both `repl` and `monitor` can be used as general-purpose serial tools - not just
284
289
 
285
290
  ### Execute Python code on device
286
291
  ```
287
- $ mpytool exec "print('Hello!')"
292
+ $ mpytool exec "print('Hello!')" # execute and stream output
288
293
  EXEC: print('Hello!')
289
294
  Hello!
290
295
 
291
296
  $ mpytool exec "import sys; print(sys.version)"
292
297
  EXEC: import sys; print(sys.version)
293
298
  3.4.0; MicroPython v1.24.0
299
+
300
+ $ mpytool exec "long_task()" -t 30 # timeout after 30 seconds
301
+ $ mpytool exec "start_server()" -t 0 # fire-and-forget (don't wait)
294
302
  ```
295
303
 
296
304
  ### Run local Python file on device
297
305
  ```
298
- $ mpytool run script.py # run script (fire-and-forget)
306
+ $ mpytool run script.py # run and stream output
307
+ RUN: script.py (128 bytes)
308
+ Hello from script!
309
+
310
+ $ mpytool run script.py -t 0 # fire-and-forget (don't wait)
299
311
  RUN: script.py (128 bytes)
300
312
 
301
- $ mpytool run script.py -- monitor # run script and capture output
313
+ $ mpytool run script.py -t 10 # timeout after 10 seconds
302
314
  RUN: script.py (128 bytes)
303
- MONITOR (Ctrl+C to stop)
304
- Hello from script!
315
+ Done!
305
316
  ```
306
317
 
307
318
  ### Edit file on device
@@ -650,7 +661,7 @@ autoload -Uz compinit && compinit
650
661
  **Quick install (or update):**
651
662
  ```bash
652
663
  mkdir -p ~/.zsh/completions
653
- curl -fsSL https://raw.githubusercontent.com/pavelrevak/mpytool/main/completions/_mpytool -o ~/.zsh/completions/_mpytool
664
+ curl -fsSL https://raw.githubusercontent.com/cortexm/mpytool/main/completions/_mpytool -o ~/.zsh/completions/_mpytool
654
665
  grep -q '\.zsh/completions' ~/.zshrc || echo 'fpath=(~/.zsh/completions $fpath); autoload -Uz compinit && compinit' >> ~/.zshrc
655
666
  exec zsh
656
667
  ```
@@ -671,17 +682,17 @@ source ~/.mpytool-completion.bash
671
682
 
672
683
  **Quick install (Linux system-wide):**
673
684
  ```bash
674
- sudo curl -fsSL https://raw.githubusercontent.com/pavelrevak/mpytool/main/completions/mpytool.bash -o /etc/bash_completion.d/mpytool && exec bash
685
+ sudo curl -fsSL https://raw.githubusercontent.com/cortexm/mpytool/main/completions/mpytool.bash -o /etc/bash_completion.d/mpytool && exec bash
675
686
  ```
676
687
 
677
688
  **Quick install (macOS Homebrew):**
678
689
  ```bash
679
- curl -fsSL https://raw.githubusercontent.com/pavelrevak/mpytool/main/completions/mpytool.bash -o /usr/local/etc/bash_completion.d/mpytool && exec bash
690
+ curl -fsSL https://raw.githubusercontent.com/cortexm/mpytool/main/completions/mpytool.bash -o /usr/local/etc/bash_completion.d/mpytool && exec bash
680
691
  ```
681
692
 
682
693
  **Quick install (user directory):**
683
694
  ```bash
684
- curl -fsSL https://raw.githubusercontent.com/pavelrevak/mpytool/main/completions/mpytool.bash -o ~/.mpytool-completion.bash
695
+ curl -fsSL https://raw.githubusercontent.com/cortexm/mpytool/main/completions/mpytool.bash -o ~/.mpytool-completion.bash
685
696
  grep -q 'mpytool-completion' ~/.bashrc || echo 'source ~/.mpytool-completion.bash' >> ~/.bashrc
686
697
  exec bash
687
698
  ```
@@ -757,4 +768,4 @@ MIT
757
768
  ### Support
758
769
 
759
770
  - Basic support is free over GitHub issues.
760
- - Professional support is available over email: [Pavel Revak](mailto:pavel.revak@gmail.com?subject=[GitHub]%20mpytool).
771
+ - Professional support is available over email: [Pavel Revak](mailto:pavelrevak@gmail.com?subject=[GitHub]%20mpytool).
@@ -1,17 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: mpytool
3
- Version: 2.3.3
4
- Summary: MPY tool - manage files on devices running MicroPython
5
- Author-email: Pavel Revak <pavel.revak@gmail.com>
6
- License-Expression: MIT
7
- Project-URL: Homepage, https://github.com/pavelrevak/mpytool
8
- Keywords: MPY,micropython
9
- Requires-Python: >=3.10
10
- Description-Content-Type: text/markdown
11
- License-File: LICENSE
12
- Requires-Dist: pyserial>=3.0
13
- Dynamic: license-file
14
-
15
1
  # mpytool
16
2
 
17
3
  MPY tool - manage files on devices running MicroPython
@@ -55,7 +41,7 @@ pip3 install mpytool
55
41
  ### Installation from git (latest development version)
56
42
 
57
43
  ```bash
58
- pip3 install git+https://github.com/pavelrevak/mpytool.git
44
+ pip3 install git+https://github.com/cortexm/mpytool.git
59
45
  ```
60
46
 
61
47
  ### Installation in virtualenv
@@ -254,7 +240,11 @@ RESET soft
254
240
  $ mpytool reset --machine # MCU reset (machine.reset, auto-reconnect)
255
241
  RESET machine
256
242
 
257
- $ 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
258
248
  RESET soft
259
249
  MONITOR (Ctrl+C to stop)
260
250
 
@@ -275,7 +265,8 @@ REPL (Ctrl+] to exit) CWD: / PATH: : :.frozen :/lib
275
265
 
276
266
  $ mpytool -p /dev/ttyUSB0 repl # specify port
277
267
  $ mpytool -b 9600 repl # specify baudrate
278
- $ 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)
279
270
  ```
280
271
 
281
272
  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.
@@ -284,24 +275,30 @@ Both `repl` and `monitor` can be used as general-purpose serial tools - not just
284
275
 
285
276
  ### Execute Python code on device
286
277
  ```
287
- $ mpytool exec "print('Hello!')"
278
+ $ mpytool exec "print('Hello!')" # execute and stream output
288
279
  EXEC: print('Hello!')
289
280
  Hello!
290
281
 
291
282
  $ mpytool exec "import sys; print(sys.version)"
292
283
  EXEC: import sys; print(sys.version)
293
284
  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)
294
288
  ```
295
289
 
296
290
  ### Run local Python file on device
297
291
  ```
298
- $ mpytool run script.py # run script (fire-and-forget)
292
+ $ mpytool run script.py # run and stream output
299
293
  RUN: script.py (128 bytes)
294
+ Hello from script!
300
295
 
301
- $ mpytool run script.py -- monitor # run script and capture output
296
+ $ mpytool run script.py -t 0 # fire-and-forget (don't wait)
302
297
  RUN: script.py (128 bytes)
303
- MONITOR (Ctrl+C to stop)
304
- Hello from script!
298
+
299
+ $ mpytool run script.py -t 10 # timeout after 10 seconds
300
+ RUN: script.py (128 bytes)
301
+ Done!
305
302
  ```
306
303
 
307
304
  ### Edit file on device
@@ -650,7 +647,7 @@ autoload -Uz compinit && compinit
650
647
  **Quick install (or update):**
651
648
  ```bash
652
649
  mkdir -p ~/.zsh/completions
653
- curl -fsSL https://raw.githubusercontent.com/pavelrevak/mpytool/main/completions/_mpytool -o ~/.zsh/completions/_mpytool
650
+ curl -fsSL https://raw.githubusercontent.com/cortexm/mpytool/main/completions/_mpytool -o ~/.zsh/completions/_mpytool
654
651
  grep -q '\.zsh/completions' ~/.zshrc || echo 'fpath=(~/.zsh/completions $fpath); autoload -Uz compinit && compinit' >> ~/.zshrc
655
652
  exec zsh
656
653
  ```
@@ -671,17 +668,17 @@ source ~/.mpytool-completion.bash
671
668
 
672
669
  **Quick install (Linux system-wide):**
673
670
  ```bash
674
- sudo curl -fsSL https://raw.githubusercontent.com/pavelrevak/mpytool/main/completions/mpytool.bash -o /etc/bash_completion.d/mpytool && exec bash
671
+ sudo curl -fsSL https://raw.githubusercontent.com/cortexm/mpytool/main/completions/mpytool.bash -o /etc/bash_completion.d/mpytool && exec bash
675
672
  ```
676
673
 
677
674
  **Quick install (macOS Homebrew):**
678
675
  ```bash
679
- curl -fsSL https://raw.githubusercontent.com/pavelrevak/mpytool/main/completions/mpytool.bash -o /usr/local/etc/bash_completion.d/mpytool && exec bash
676
+ curl -fsSL https://raw.githubusercontent.com/cortexm/mpytool/main/completions/mpytool.bash -o /usr/local/etc/bash_completion.d/mpytool && exec bash
680
677
  ```
681
678
 
682
679
  **Quick install (user directory):**
683
680
  ```bash
684
- curl -fsSL https://raw.githubusercontent.com/pavelrevak/mpytool/main/completions/mpytool.bash -o ~/.mpytool-completion.bash
681
+ curl -fsSL https://raw.githubusercontent.com/cortexm/mpytool/main/completions/mpytool.bash -o ~/.mpytool-completion.bash
685
682
  grep -q 'mpytool-completion' ~/.bashrc || echo 'source ~/.mpytool-completion.bash' >> ~/.bashrc
686
683
  exec bash
687
684
  ```
@@ -757,4 +754,4 @@ MIT
757
754
  ### Support
758
755
 
759
756
  - Basic support is free over GitHub issues.
760
- - Professional support is available over email: [Pavel Revak](mailto:pavel.revak@gmail.com?subject=[GitHub]%20mpytool).
757
+ - Professional support is available over email: [Pavel Revak](mailto:pavelrevak@gmail.com?subject=[GitHub]%20mpytool).
@@ -3,6 +3,6 @@
3
3
  from mpytool.conn import ConnError, Timeout
4
4
  from mpytool.conn_serial import ConnSerial
5
5
  from mpytool.conn_socket import ConnSocket
6
- from mpytool.mpy_comm import MpyError, CmdError
6
+ from mpytool.mpy_comm import MpyComm, MpyError, CmdError
7
7
  from mpytool.mpy import Mpy, PathNotFound, FileNotFound, DirNotFound
8
8
  from mpytool.logger import SimpleColorLogger
@@ -22,14 +22,11 @@ class ConnSerial(_conn.Conn):
22
22
  # Windows: pyserial has no fd, select() works only with sockets
23
23
  if not hasattr(self._serial, 'fd'):
24
24
  self._has_data = self._has_data_polling
25
- # Debug info about port type
25
+ # Debug info about port
26
26
  port = serial_config.get('port', '')
27
27
  if self._port_info and self._port_info.vid:
28
- vid = self._port_info.vid
29
- pid = self._port_info.pid
30
- self._log.info(
31
- "Connected to %s [%04X:%04X %s]",
32
- port, vid, pid, self.port_type)
28
+ desc = _utils._port_description(self._port_info)
29
+ self._log.info("Connected to %s [%s]", port, desc)
33
30
  else:
34
31
  self._log.info("Connected to %s [unknown serial port]", port)
35
32
 
@@ -179,7 +179,7 @@ CMD_MAX = CMD_READLINE
179
179
  _ESCAPE_BYTE = bytes([ESCAPE])
180
180
 
181
181
  _SOFT_REBOOT = b'soft reboot'
182
- _REPL_PROMPT = b'>>> '
182
+ from mpytool.mpy_comm import REPL_PROMPT as _REPL_PROMPT
183
183
  _REBOOT_BUF_MAX = 256
184
184
  _REBOOT_BUF_KEEP = 64
185
185
  _LISTDIR_LIMIT = 1000 # max entries returned by CMD_LISTDIR
@@ -1,5 +1,6 @@
1
1
  """MicroPython tool: MPY communication"""
2
2
 
3
+ import io as _io
3
4
  import re as _re
4
5
  import time as _time
5
6
 
@@ -13,6 +14,10 @@ CTRL_C = b'\x03' # Interrupt
13
14
  CTRL_D = b'\x04' # Execute / Soft reset / End raw-paste
14
15
  CTRL_E = b'\x05' # Paste mode
15
16
 
17
+ # REPL prompt
18
+ REPL_PROMPT = b'>>> '
19
+ REPL_PROMPT_LINE = b'\r\n' + REPL_PROMPT
20
+
16
21
  # Raw-paste mode
17
22
  RAW_PASTE_ENTER = CTRL_E + b'A' + CTRL_A # Enter raw-paste mode sequence
18
23
  RAW_PASTE_ACK = b'\x01' # Flow control ACK
@@ -121,7 +126,7 @@ class MpyComm():
121
126
  if attempt >= 4:
122
127
  self._conn.write(b'\x18')
123
128
  try:
124
- self._conn.read_until(b'\r\n>>> ', timeout=0.2)
129
+ self._conn.read_until(REPL_PROMPT_LINE, timeout=0.2)
125
130
  self._repl_mode = False
126
131
  return True
127
132
  except _conn.Timeout:
@@ -149,7 +154,7 @@ class MpyComm():
149
154
  return
150
155
  self._log.info('EXIT RAW REPL')
151
156
  self._conn.write(CTRL_B)
152
- self._conn.read_until(b'\r\n>>> ')
157
+ self._conn.read_until(REPL_PROMPT_LINE)
153
158
  self._repl_mode = False
154
159
 
155
160
  def soft_reset(self):
@@ -176,16 +181,64 @@ class MpyComm():
176
181
  self._repl_mode = None
177
182
  self._raw_paste_supported = None
178
183
 
179
- def exec(self, command, timeout=None):
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
+
230
+ def exec(self, command, timeout=None, stream=False):
180
231
  """Execute command
181
232
 
182
233
  Arguments:
183
234
  command: command to execute
184
235
  timeout: maximum waiting time for result (None = wait forever),
185
236
  0 = submit only (send code, don't wait for output)
237
+ stream: False = return bytes, True = yield chunks,
238
+ file-like object = write chunks to it and return b''
186
239
 
187
240
  Returns:
188
- command STDOUT result
241
+ bytes result, generator, or b'' (when stream is file-like)
189
242
 
190
243
  Raises:
191
244
  CmdError when command return error
@@ -199,6 +252,10 @@ class MpyComm():
199
252
  if timeout == 0:
200
253
  self._repl_mode = False
201
254
  return b''
255
+ if stream is True:
256
+ return self._exec_stream_result(command, timeout)
257
+ if stream:
258
+ return self._exec_stream_to(command, timeout, stream)
202
259
  result = self._conn.read_until(CTRL_D, timeout)
203
260
  if result:
204
261
  self._log.info('RES: %s', bytes(result))
@@ -280,6 +337,65 @@ class MpyComm():
280
337
  if byte == CTRL_D:
281
338
  break
282
339
 
340
+ def _read_stdout_chunks(self, timeout):
341
+ """Read stdout chunks until CTRL_D marker.
342
+
343
+ Yields (chunk, is_last) tuples. Timeout is total wall time.
344
+ """
345
+ start_time = _time.time()
346
+ while True:
347
+ if timeout is not None:
348
+ remaining = timeout - (_time.time() - start_time)
349
+ if remaining <= 0:
350
+ raise _conn.Timeout("Execution timeout")
351
+ self._conn._read_to_buffer(wait_timeout=0.001)
352
+ idx = self._conn._buffer.find(CTRL_D)
353
+ if idx != -1:
354
+ chunk = bytes(self._conn._buffer[:idx])
355
+ del self._conn._buffer[:idx + len(CTRL_D)]
356
+ if chunk:
357
+ yield chunk
358
+ return
359
+ if self._conn._buffer:
360
+ yield bytes(self._conn._buffer)
361
+ self._conn._buffer.clear()
362
+
363
+ def _check_stderr(self, command, result, timeout, start_time):
364
+ """Read stderr and raise CmdError if non-empty."""
365
+ if result:
366
+ self._log.info('RES: %s', bytes(result))
367
+ err_timeout = 5 if timeout is None else max(
368
+ 1, timeout - (_time.time() - start_time))
369
+ err = self._conn.read_until(CTRL_D + b'>', err_timeout)
370
+ if err:
371
+ raise CmdError(command, bytes(result), err)
372
+
373
+ def _exec_stream_result(self, command, timeout):
374
+ """Read execution result as streaming generator."""
375
+ start_time = _time.time()
376
+ result = bytearray()
377
+ for chunk in self._read_stdout_chunks(timeout):
378
+ result.extend(chunk)
379
+ yield chunk
380
+ self._check_stderr(command, result, timeout, start_time)
381
+
382
+ def _exec_stream_to(self, command, timeout, out):
383
+ """Read execution result, writing chunks to file-like object.
384
+
385
+ Supports both binary and text streams (io.TextIOBase).
386
+ """
387
+ start_time = _time.time()
388
+ result = bytearray()
389
+ text_mode = isinstance(out, _io.TextIOBase)
390
+ for chunk in self._read_stdout_chunks(timeout):
391
+ result.extend(chunk)
392
+ out.write(
393
+ chunk.decode('utf-8', 'backslashreplace')
394
+ if text_mode else chunk)
395
+ out.flush()
396
+ self._check_stderr(command, result, timeout, start_time)
397
+ return b''
398
+
283
399
  def _read_execution_result(self, command, timeout):
284
400
  """Read and parse execution result."""
285
401
  result = self._conn.read_until(CTRL_D, timeout)
@@ -290,15 +406,17 @@ class MpyComm():
290
406
  raise CmdError(command.decode('utf-8', errors='replace'), result, err)
291
407
  return result
292
408
 
293
- def exec_raw_paste(self, command, timeout=5):
409
+ def exec_raw_paste(self, command, timeout=5, stream=False):
294
410
  """Execute code via raw-paste mode (flow-controlled, less RAM).
295
411
 
296
412
  Arguments:
297
413
  command: code to execute (str or bytes)
298
414
  timeout: max wait time (0 = submit only, don't wait for output)
415
+ stream: False = return bytes, True = yield chunks,
416
+ file-like object = write chunks to it and return b''
299
417
 
300
418
  Returns:
301
- command STDOUT result
419
+ bytes result, generator, or b'' (when stream is file-like)
302
420
 
303
421
  Raises:
304
422
  CmdError: command execution error
@@ -338,26 +456,32 @@ class MpyComm():
338
456
  self._repl_mode = False
339
457
  return b''
340
458
 
459
+ if stream is True:
460
+ return self._exec_stream_result(command, timeout)
461
+ if stream:
462
+ return self._exec_stream_to(command, timeout, stream)
341
463
  return self._read_execution_result(command, timeout)
342
464
 
343
- def try_raw_paste(self, command, timeout=5):
465
+ def try_raw_paste(self, command, timeout=5, stream=False):
344
466
  """Try raw-paste mode, fall back to regular exec if not supported.
345
467
 
346
468
  Arguments:
347
469
  command: command to execute
348
470
  timeout: maximum waiting time for result
471
+ stream: False = return bytes, True = yield chunks,
472
+ file-like object = write chunks to it and return b''
349
473
 
350
474
  Returns:
351
- command STDOUT result
475
+ bytes result, generator, or b'' (when stream is file-like)
352
476
  """
353
477
  # If we know raw-paste is not supported, skip it
354
478
  if self._raw_paste_supported is False:
355
- return self.exec(command, timeout)
479
+ return self.exec(command, timeout, stream=stream)
356
480
 
357
481
  try:
358
- return self.exec_raw_paste(command, timeout)
482
+ return self.exec_raw_paste(command, timeout, stream=stream)
359
483
  except MpyError as e:
360
484
  if "not supported" in str(e):
361
485
  self._log.info("Raw-paste not supported, using regular exec")
362
- return self.exec(command, timeout)
486
+ return self.exec(command, timeout, stream=stream)
363
487
  raise