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.
- {mpytool-2.3.3/mpytool.egg-info → mpytool-2.3.4}/PKG-INFO +27 -16
- mpytool-2.3.3/PKG-INFO → mpytool-2.3.4/README.md +24 -27
- {mpytool-2.3.3 → mpytool-2.3.4}/mpytool/__init__.py +1 -1
- {mpytool-2.3.3 → mpytool-2.3.4}/mpytool/conn_serial.py +3 -6
- {mpytool-2.3.3 → mpytool-2.3.4}/mpytool/mount.py +1 -1
- {mpytool-2.3.3 → mpytool-2.3.4}/mpytool/mpy_comm.py +135 -11
- {mpytool-2.3.3 → mpytool-2.3.4}/mpytool/mpytool.py +80 -28
- {mpytool-2.3.3 → mpytool-2.3.4}/mpytool/utils.py +78 -8
- mpytool-2.3.3/README.md → mpytool-2.3.4/mpytool.egg-info/PKG-INFO +38 -13
- {mpytool-2.3.3 → mpytool-2.3.4}/pyproject.toml +3 -3
- {mpytool-2.3.3 → mpytool-2.3.4}/tests/test_mpy.py +355 -0
- {mpytool-2.3.3 → mpytool-2.3.4}/tests/test_mpytool.py +69 -27
- {mpytool-2.3.3 → mpytool-2.3.4}/tests/test_utils.py +96 -0
- {mpytool-2.3.3 → mpytool-2.3.4}/LICENSE +0 -0
- {mpytool-2.3.3 → mpytool-2.3.4}/mpytool/cmd_cp.py +0 -0
- {mpytool-2.3.3 → mpytool-2.3.4}/mpytool/conn.py +0 -0
- {mpytool-2.3.3 → mpytool-2.3.4}/mpytool/conn_socket.py +0 -0
- {mpytool-2.3.3 → mpytool-2.3.4}/mpytool/logger.py +0 -0
- {mpytool-2.3.3 → mpytool-2.3.4}/mpytool/mpy.py +0 -0
- {mpytool-2.3.3 → mpytool-2.3.4}/mpytool/mpy_cross.py +0 -0
- {mpytool-2.3.3 → mpytool-2.3.4}/mpytool/speedtest.py +0 -0
- {mpytool-2.3.3 → mpytool-2.3.4}/mpytool/terminal.py +0 -0
- {mpytool-2.3.3 → mpytool-2.3.4}/mpytool/terminal_unix.py +0 -0
- {mpytool-2.3.3 → mpytool-2.3.4}/mpytool/terminal_win.py +0 -0
- {mpytool-2.3.3 → mpytool-2.3.4}/mpytool.egg-info/SOURCES.txt +0 -0
- {mpytool-2.3.3 → mpytool-2.3.4}/mpytool.egg-info/dependency_links.txt +0 -0
- {mpytool-2.3.3 → mpytool-2.3.4}/mpytool.egg-info/entry_points.txt +0 -0
- {mpytool-2.3.3 → mpytool-2.3.4}/mpytool.egg-info/requires.txt +0 -0
- {mpytool-2.3.3 → mpytool-2.3.4}/mpytool.egg-info/top_level.txt +0 -0
- {mpytool-2.3.3 → mpytool-2.3.4}/setup.cfg +0 -0
- {mpytool-2.3.3 → mpytool-2.3.4}/tests/test_cli.py +0 -0
- {mpytool-2.3.3 → mpytool-2.3.4}/tests/test_cmd_cp.py +0 -0
- {mpytool-2.3.3 → mpytool-2.3.4}/tests/test_errors.py +0 -0
- {mpytool-2.3.3 → mpytool-2.3.4}/tests/test_integration.py +0 -0
- {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
|
+
Version: 2.3.4
|
|
4
4
|
Summary: MPY tool - manage files on devices running MicroPython
|
|
5
|
-
Author-email: Pavel Revak <
|
|
5
|
+
Author-email: Pavel Revak <pavelrevak@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
7
|
-
Project-URL: Homepage, https://github.com/
|
|
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/
|
|
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
|
|
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
|
|
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
|
|
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
|
|
313
|
+
$ mpytool run script.py -t 10 # timeout after 10 seconds
|
|
302
314
|
RUN: script.py (128 bytes)
|
|
303
|
-
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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:
|
|
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/
|
|
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
|
|
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
|
|
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
|
|
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
|
|
296
|
+
$ mpytool run script.py -t 0 # fire-and-forget (don't wait)
|
|
302
297
|
RUN: script.py (128 bytes)
|
|
303
|
-
|
|
304
|
-
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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:
|
|
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
|
|
25
|
+
# Debug info about port
|
|
26
26
|
port = serial_config.get('port', '')
|
|
27
27
|
if self._port_info and self._port_info.vid:
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|