mpytool 2.3.6__tar.gz → 2.4.0__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.6 → mpytool-2.4.0}/PKG-INFO +34 -4
- {mpytool-2.3.6 → mpytool-2.4.0}/README.md +33 -3
- {mpytool-2.3.6 → mpytool-2.4.0}/README_API.md +53 -5
- {mpytool-2.3.6 → mpytool-2.4.0}/completions/_mpytool +4 -0
- {mpytool-2.3.6 → mpytool-2.4.0}/completions/mpytool.bash +4 -0
- mpytool-2.4.0/mpytool/_version.py +24 -0
- {mpytool-2.3.6 → mpytool-2.4.0}/mpytool/conn.py +7 -0
- {mpytool-2.3.6 → mpytool-2.4.0}/mpytool/conn_serial.py +13 -0
- {mpytool-2.3.6 → mpytool-2.4.0}/mpytool/conn_socket.py +29 -3
- {mpytool-2.3.6 → mpytool-2.4.0}/mpytool/mpy.py +23 -0
- {mpytool-2.3.6 → mpytool-2.4.0}/mpytool/mpytool.py +60 -6
- {mpytool-2.3.6 → mpytool-2.4.0}/mpytool/terminal.py +15 -1
- mpytool-2.4.0/mpytool/terminal_unix.py +65 -0
- {mpytool-2.3.6 → mpytool-2.4.0}/mpytool/terminal_win.py +3 -0
- {mpytool-2.3.6 → mpytool-2.4.0}/mpytool.egg-info/PKG-INFO +34 -4
- {mpytool-2.3.6 → mpytool-2.4.0}/mpytool.egg-info/SOURCES.txt +1 -0
- {mpytool-2.3.6 → mpytool-2.4.0}/tests/test_cli.py +14 -3
- mpytool-2.4.0/tests/test_conn_socket.py +307 -0
- {mpytool-2.3.6 → mpytool-2.4.0}/tests/test_integration.py +4 -2
- {mpytool-2.3.6 → mpytool-2.4.0}/tests/test_mpy.py +55 -0
- mpytool-2.3.6/mpytool/_version.py +0 -34
- mpytool-2.3.6/mpytool/terminal_unix.py +0 -43
- {mpytool-2.3.6 → mpytool-2.4.0}/.github/workflows/integration.yml +0 -0
- {mpytool-2.3.6 → mpytool-2.4.0}/.github/workflows/publish.yml +0 -0
- {mpytool-2.3.6 → mpytool-2.4.0}/.github/workflows/tests.yml +0 -0
- {mpytool-2.3.6 → mpytool-2.4.0}/.gitignore +0 -0
- {mpytool-2.3.6 → mpytool-2.4.0}/LICENSE +0 -0
- {mpytool-2.3.6 → mpytool-2.4.0}/README_bench.md +0 -0
- {mpytool-2.3.6 → mpytool-2.4.0}/README_mpremote.md +0 -0
- {mpytool-2.3.6 → mpytool-2.4.0}/mpytool/__init__.py +0 -0
- {mpytool-2.3.6 → mpytool-2.4.0}/mpytool/cmd_cp.py +0 -0
- {mpytool-2.3.6 → mpytool-2.4.0}/mpytool/logger.py +0 -0
- {mpytool-2.3.6 → mpytool-2.4.0}/mpytool/mount.py +0 -0
- {mpytool-2.3.6 → mpytool-2.4.0}/mpytool/mpy_comm.py +0 -0
- {mpytool-2.3.6 → mpytool-2.4.0}/mpytool/mpy_cross.py +0 -0
- {mpytool-2.3.6 → mpytool-2.4.0}/mpytool/speedtest.py +0 -0
- {mpytool-2.3.6 → mpytool-2.4.0}/mpytool/utils.py +0 -0
- {mpytool-2.3.6 → mpytool-2.4.0}/mpytool.egg-info/dependency_links.txt +0 -0
- {mpytool-2.3.6 → mpytool-2.4.0}/mpytool.egg-info/entry_points.txt +0 -0
- {mpytool-2.3.6 → mpytool-2.4.0}/mpytool.egg-info/requires.txt +0 -0
- {mpytool-2.3.6 → mpytool-2.4.0}/mpytool.egg-info/top_level.txt +0 -0
- {mpytool-2.3.6 → mpytool-2.4.0}/pyproject.toml +0 -0
- {mpytool-2.3.6 → mpytool-2.4.0}/setup.cfg +0 -0
- {mpytool-2.3.6 → mpytool-2.4.0}/tests/__init__.py +0 -0
- {mpytool-2.3.6 → mpytool-2.4.0}/tests/test_cmd_cp.py +0 -0
- {mpytool-2.3.6 → mpytool-2.4.0}/tests/test_errors.py +0 -0
- {mpytool-2.3.6 → mpytool-2.4.0}/tests/test_helpers.py +0 -0
- {mpytool-2.3.6 → mpytool-2.4.0}/tests/test_mount.py +0 -0
- {mpytool-2.3.6 → mpytool-2.4.0}/tests/test_mpytool.py +0 -0
- {mpytool-2.3.6 → mpytool-2.4.0}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mpytool
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.0
|
|
4
4
|
Summary: MPY tool - manage files on devices running MicroPython
|
|
5
5
|
Author-email: Pavel Revak <pavelrevak@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -31,6 +31,8 @@ It is an alternative to the official [mpremote](https://docs.micropython.org/en/
|
|
|
31
31
|
(CP2102, CH340)
|
|
32
32
|
- **Multiple reset options** - soft, MCU, hardware (RTS),
|
|
33
33
|
bootloader entry
|
|
34
|
+
- **Wipe device** - erase the whole filesystem and machine-reset
|
|
35
|
+
for a clean slate (handy at the start of automated tests)
|
|
34
36
|
- **General-purpose serial terminal** - `repl` and `monitor` work
|
|
35
37
|
with any serial device
|
|
36
38
|
- **Mount local directory** - VFS mount (read-only or read-write)
|
|
@@ -42,7 +44,7 @@ It is an alternative to the official [mpremote](https://docs.micropython.org/en/
|
|
|
42
44
|
- **Raw-paste mode** - flow-controlled code execution with reduced
|
|
43
45
|
RAM usage (API)
|
|
44
46
|
- **Shell completion** - ZSH and Bash with remote path completion
|
|
45
|
-
- **Network support** - connect over TCP
|
|
47
|
+
- **Network support** - connect over TCP with optional SSL/TLS
|
|
46
48
|
- **Full Unicode support** - handles Unicode in file names and content
|
|
47
49
|
|
|
48
50
|
See [README_mpremote.md](README_mpremote.md) for detailed comparison with mpremote.
|
|
@@ -270,6 +272,30 @@ $ mpytool sleep 2 # sleep for 2 seconds (useful between commands)
|
|
|
270
272
|
SLEEP 2.0s
|
|
271
273
|
```
|
|
272
274
|
|
|
275
|
+
### Wipe device (erase all files + machine reset)
|
|
276
|
+
```
|
|
277
|
+
$ mpytool wipe # delete everything from / then machine.reset()
|
|
278
|
+
WIPE
|
|
279
|
+
rm /boot.py
|
|
280
|
+
rm /main.py
|
|
281
|
+
rm /lib
|
|
282
|
+
reset
|
|
283
|
+
|
|
284
|
+
$ mpytool wipe -- ls # wipe, reconnect, then list (now empty)
|
|
285
|
+
WIPE
|
|
286
|
+
rm /main.py
|
|
287
|
+
reset, reconnected
|
|
288
|
+
LS:
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
`wipe` erases the whole filesystem (recursive delete of `/`) and then does a
|
|
292
|
+
full `machine.reset()` — a soft reset would leave open sockets, WiFi and
|
|
293
|
+
mounted VFS alive, so machine reset is used for a true clean slate. It **never
|
|
294
|
+
prompts** (mpytool has no interactive mode); `-y`/`--yes` is accepted as a
|
|
295
|
+
no-op. When `wipe` is the last command it just resets and exits; when more
|
|
296
|
+
commands follow it reconnects first (use `-t SECONDS` for the reconnect
|
|
297
|
+
timeout). See [README_API.md](README_API.md) for `Mpy.wipe()`.
|
|
298
|
+
|
|
273
299
|
### Serial terminal and monitor (general purpose)
|
|
274
300
|
```
|
|
275
301
|
$ mpytool repl # auto-detect port, 115200 baud
|
|
@@ -537,8 +563,12 @@ TREE: /
|
|
|
537
563
|
|
|
538
564
|
### Connect over network (TCP, default port 23)
|
|
539
565
|
```
|
|
540
|
-
$ mpytool -a 192.168.1.100 ls
|
|
541
|
-
$ mpytool -a 192.168.1.100:8266 tree
|
|
566
|
+
$ mpytool -a 192.168.1.100 ls # plain TCP
|
|
567
|
+
$ mpytool -a 192.168.1.100:8266 tree # custom port
|
|
568
|
+
$ mpytool -a 192.168.1.100:8023 --ssl ls # SSL/TLS connection
|
|
569
|
+
$ mpytool -a 192.168.1.100:8023 --ssl-ca ca.pem ls # SSL with custom CA
|
|
570
|
+
$ mpytool -a 192.168.1.100:8023 --ssl-no-hostname ls # SSL, skip hostname check
|
|
571
|
+
$ mpytool -a 192.168.1.100:8023 --ssl-no-verify ls # SSL, skip all verification
|
|
542
572
|
```
|
|
543
573
|
|
|
544
574
|
### Set baudrate (default 115200)
|
|
@@ -16,6 +16,8 @@ It is an alternative to the official [mpremote](https://docs.micropython.org/en/
|
|
|
16
16
|
(CP2102, CH340)
|
|
17
17
|
- **Multiple reset options** - soft, MCU, hardware (RTS),
|
|
18
18
|
bootloader entry
|
|
19
|
+
- **Wipe device** - erase the whole filesystem and machine-reset
|
|
20
|
+
for a clean slate (handy at the start of automated tests)
|
|
19
21
|
- **General-purpose serial terminal** - `repl` and `monitor` work
|
|
20
22
|
with any serial device
|
|
21
23
|
- **Mount local directory** - VFS mount (read-only or read-write)
|
|
@@ -27,7 +29,7 @@ It is an alternative to the official [mpremote](https://docs.micropython.org/en/
|
|
|
27
29
|
- **Raw-paste mode** - flow-controlled code execution with reduced
|
|
28
30
|
RAM usage (API)
|
|
29
31
|
- **Shell completion** - ZSH and Bash with remote path completion
|
|
30
|
-
- **Network support** - connect over TCP
|
|
32
|
+
- **Network support** - connect over TCP with optional SSL/TLS
|
|
31
33
|
- **Full Unicode support** - handles Unicode in file names and content
|
|
32
34
|
|
|
33
35
|
See [README_mpremote.md](README_mpremote.md) for detailed comparison with mpremote.
|
|
@@ -255,6 +257,30 @@ $ mpytool sleep 2 # sleep for 2 seconds (useful between commands)
|
|
|
255
257
|
SLEEP 2.0s
|
|
256
258
|
```
|
|
257
259
|
|
|
260
|
+
### Wipe device (erase all files + machine reset)
|
|
261
|
+
```
|
|
262
|
+
$ mpytool wipe # delete everything from / then machine.reset()
|
|
263
|
+
WIPE
|
|
264
|
+
rm /boot.py
|
|
265
|
+
rm /main.py
|
|
266
|
+
rm /lib
|
|
267
|
+
reset
|
|
268
|
+
|
|
269
|
+
$ mpytool wipe -- ls # wipe, reconnect, then list (now empty)
|
|
270
|
+
WIPE
|
|
271
|
+
rm /main.py
|
|
272
|
+
reset, reconnected
|
|
273
|
+
LS:
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
`wipe` erases the whole filesystem (recursive delete of `/`) and then does a
|
|
277
|
+
full `machine.reset()` — a soft reset would leave open sockets, WiFi and
|
|
278
|
+
mounted VFS alive, so machine reset is used for a true clean slate. It **never
|
|
279
|
+
prompts** (mpytool has no interactive mode); `-y`/`--yes` is accepted as a
|
|
280
|
+
no-op. When `wipe` is the last command it just resets and exits; when more
|
|
281
|
+
commands follow it reconnects first (use `-t SECONDS` for the reconnect
|
|
282
|
+
timeout). See [README_API.md](README_API.md) for `Mpy.wipe()`.
|
|
283
|
+
|
|
258
284
|
### Serial terminal and monitor (general purpose)
|
|
259
285
|
```
|
|
260
286
|
$ mpytool repl # auto-detect port, 115200 baud
|
|
@@ -522,8 +548,12 @@ TREE: /
|
|
|
522
548
|
|
|
523
549
|
### Connect over network (TCP, default port 23)
|
|
524
550
|
```
|
|
525
|
-
$ mpytool -a 192.168.1.100 ls
|
|
526
|
-
$ mpytool -a 192.168.1.100:8266 tree
|
|
551
|
+
$ mpytool -a 192.168.1.100 ls # plain TCP
|
|
552
|
+
$ mpytool -a 192.168.1.100:8266 tree # custom port
|
|
553
|
+
$ mpytool -a 192.168.1.100:8023 --ssl ls # SSL/TLS connection
|
|
554
|
+
$ mpytool -a 192.168.1.100:8023 --ssl-ca ca.pem ls # SSL with custom CA
|
|
555
|
+
$ mpytool -a 192.168.1.100:8023 --ssl-no-hostname ls # SSL, skip hostname check
|
|
556
|
+
$ mpytool -a 192.168.1.100:8023 --ssl-no-verify ls # SSL, skip all verification
|
|
527
557
|
```
|
|
528
558
|
|
|
529
559
|
### Set baudrate (default 115200)
|
|
@@ -107,25 +107,44 @@ conn.reconnect(timeout=5) # Reconnect after device reset (USB-CDC)
|
|
|
107
107
|
|
|
108
108
|
### ConnSocket
|
|
109
109
|
|
|
110
|
-
TCP socket connection for network-connected devices.
|
|
110
|
+
TCP socket connection for network-connected devices with optional SSL/TLS.
|
|
111
111
|
|
|
112
112
|
```python
|
|
113
|
-
mpytool.ConnSocket(address, log=None
|
|
113
|
+
mpytool.ConnSocket(address, log=None, ssl=False, ssl_verify=True,
|
|
114
|
+
ssl_ca=None, ssl_check_hostname=True)
|
|
114
115
|
```
|
|
115
116
|
|
|
116
117
|
**Parameters:**
|
|
117
118
|
- `address` (str): Host address with optional port (e.g., `192.168.1.100` or `192.168.1.100:8266`)
|
|
118
119
|
- `log`: Optional logger instance
|
|
120
|
+
- `ssl` (bool): Enable SSL/TLS encryption (default: `False`)
|
|
121
|
+
- `ssl_verify` (bool): Verify server certificate (default: `True`). Set to `False` for self-signed certs.
|
|
122
|
+
- `ssl_ca` (str): Path to CA certificate file for custom certificate authorities
|
|
123
|
+
- `ssl_check_hostname` (bool): Verify hostname matches certificate (default: `True`). Set to `False` when one cert is shared across multiple hosts.
|
|
119
124
|
|
|
120
125
|
Default port is 23 if not specified.
|
|
121
126
|
|
|
122
|
-
**
|
|
127
|
+
**Examples:**
|
|
123
128
|
```python
|
|
129
|
+
# Plain TCP
|
|
124
130
|
conn = mpytool.ConnSocket(address='192.168.1.100:8266')
|
|
131
|
+
|
|
132
|
+
# SSL with system CA
|
|
133
|
+
conn = mpytool.ConnSocket(address='server:8023', ssl=True)
|
|
134
|
+
|
|
135
|
+
# SSL with custom CA
|
|
136
|
+
conn = mpytool.ConnSocket(address='server:8023', ssl=True, ssl_ca='ca.pem')
|
|
137
|
+
|
|
138
|
+
# SSL, verify cert but skip hostname check
|
|
139
|
+
conn = mpytool.ConnSocket(address='192.168.1.100:8023',
|
|
140
|
+
ssl=True, ssl_ca='ca.pem', ssl_check_hostname=False)
|
|
141
|
+
|
|
142
|
+
# SSL without any verification (insecure)
|
|
143
|
+
conn = mpytool.ConnSocket(address='server:8023', ssl=True, ssl_verify=False)
|
|
125
144
|
```
|
|
126
145
|
|
|
127
146
|
**Raises:**
|
|
128
|
-
- `ConnError`: If unable to connect
|
|
147
|
+
- `ConnError`: If unable to connect or SSL handshake fails
|
|
129
148
|
|
|
130
149
|
### Common Connection Methods
|
|
131
150
|
|
|
@@ -873,7 +892,7 @@ Soft reset in raw REPL mode (clears RAM only, doesn't run boot.py/main.py).
|
|
|
873
892
|
mpy.soft_reset_raw()
|
|
874
893
|
```
|
|
875
894
|
|
|
876
|
-
#### machine_reset(reconnect=True)
|
|
895
|
+
#### machine_reset(reconnect=True, timeout=None)
|
|
877
896
|
|
|
878
897
|
MCU reset using `machine.reset()`.
|
|
879
898
|
|
|
@@ -883,6 +902,7 @@ mpy.machine_reset(reconnect=True)
|
|
|
883
902
|
|
|
884
903
|
**Parameters:**
|
|
885
904
|
- `reconnect` (bool): If True, attempt to reconnect after reset (for USB-CDC ports)
|
|
905
|
+
- `timeout` (int): Reconnect timeout in seconds (None = default)
|
|
886
906
|
|
|
887
907
|
#### hard_reset()
|
|
888
908
|
|
|
@@ -895,6 +915,34 @@ mpy.hard_reset()
|
|
|
895
915
|
**Raises:**
|
|
896
916
|
- `NotImplementedError`: If connection doesn't support hardware reset
|
|
897
917
|
|
|
918
|
+
#### wipe(reconnect=True, timeout=None, on_delete=None)
|
|
919
|
+
|
|
920
|
+
Erase all files from the filesystem root, then `machine.reset()` the device.
|
|
921
|
+
|
|
922
|
+
Recursively deletes every entry in `/`, then performs a full machine reset so
|
|
923
|
+
runtime state (open sockets, WiFi, mounted VFS) is dropped too — a soft reset
|
|
924
|
+
would leave those alive. Intended for a clean slate, e.g. at the start of
|
|
925
|
+
integration tests.
|
|
926
|
+
|
|
927
|
+
```python
|
|
928
|
+
# Clean slate at the start of a test
|
|
929
|
+
mpy.wipe()
|
|
930
|
+
|
|
931
|
+
# Wipe without waiting to reconnect
|
|
932
|
+
mpy.wipe(reconnect=False)
|
|
933
|
+
|
|
934
|
+
# Show what gets removed
|
|
935
|
+
mpy.wipe(on_delete=lambda path: print(f"removing {path}"))
|
|
936
|
+
```
|
|
937
|
+
|
|
938
|
+
**Parameters:**
|
|
939
|
+
- `reconnect` (bool): If True, wait for the device to come back after reset
|
|
940
|
+
- `timeout` (int): Reconnect timeout in seconds (None = default)
|
|
941
|
+
- `on_delete` (callable): Optional `callback(path)` invoked before each delete
|
|
942
|
+
|
|
943
|
+
**Returns:**
|
|
944
|
+
- `bool`: True if reconnected after reset, False otherwise
|
|
945
|
+
|
|
898
946
|
### Flash/Partition Methods
|
|
899
947
|
|
|
900
948
|
#### partitions()
|
|
@@ -133,6 +133,10 @@ _mpytool() {
|
|
|
133
133
|
'--port[serial port]:port:_mpytool_ports' \
|
|
134
134
|
'-a[network address]:address:' \
|
|
135
135
|
'--address[network address]:address:' \
|
|
136
|
+
'--ssl[use SSL/TLS]' \
|
|
137
|
+
'--ssl-no-verify[disable SSL certificate verification]' \
|
|
138
|
+
'--ssl-ca[CA certificate file]:file:_files' \
|
|
139
|
+
'--ssl-no-hostname[disable SSL hostname verification]' \
|
|
136
140
|
'-b[baud rate]:baud:(9600 19200 38400 57600 115200 230400 460800 921600)' \
|
|
137
141
|
'--baud[baud rate]:baud:(9600 19200 38400 57600 115200 230400 460800 921600)' \
|
|
138
142
|
'-d[debug level]' \
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '2.4.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 4, 0)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = 'gf2ecc3ebb'
|
|
@@ -208,6 +208,13 @@ class Conn():
|
|
|
208
208
|
def close(self):
|
|
209
209
|
"""Close connection"""
|
|
210
210
|
|
|
211
|
+
def discard_output(self):
|
|
212
|
+
"""Discard buffered output not yet transmitted (no-op by default).
|
|
213
|
+
|
|
214
|
+
Serial connections override this; needed before closing a port with
|
|
215
|
+
undeliverable queued data, otherwise close() blocks waiting to drain.
|
|
216
|
+
"""
|
|
217
|
+
|
|
211
218
|
def hard_reset(self):
|
|
212
219
|
"""Hardware reset (only available on serial connections)"""
|
|
213
220
|
raise NotImplementedError("Hardware reset not available on this connection")
|
|
@@ -46,6 +46,19 @@ class ConnSerial(_conn.Conn):
|
|
|
46
46
|
def __del__(self):
|
|
47
47
|
self.close()
|
|
48
48
|
|
|
49
|
+
def discard_output(self):
|
|
50
|
+
"""Discard kernel TX buffer (data written but not yet transmitted).
|
|
51
|
+
|
|
52
|
+
Without this, closing the port blocks while the kernel waits to drain
|
|
53
|
+
output the device never consumed (e.g. keystrokes sent to a device not
|
|
54
|
+
reading its USB-CDC, after leaving the REPL).
|
|
55
|
+
"""
|
|
56
|
+
if self._serial:
|
|
57
|
+
try:
|
|
58
|
+
self._serial.reset_output_buffer()
|
|
59
|
+
except OSError:
|
|
60
|
+
pass
|
|
61
|
+
|
|
49
62
|
@property
|
|
50
63
|
def fd(self):
|
|
51
64
|
return self._serial.fd if self._serial else None
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
"""MicroPython tool: socket connector"""
|
|
2
2
|
|
|
3
3
|
import socket as _socket
|
|
4
|
+
import ssl as _ssl
|
|
4
5
|
import mpytool.conn as _conn
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
class ConnSocket(_conn.Conn):
|
|
8
|
-
def __init__(
|
|
9
|
+
def __init__(
|
|
10
|
+
self, address, log=None, ssl=False, ssl_verify=True,
|
|
11
|
+
ssl_ca=None, ssl_check_hostname=True):
|
|
9
12
|
super().__init__(log)
|
|
10
13
|
self._socket = None
|
|
11
14
|
sock = _socket.socket(_socket.AF_INET, _socket.SOCK_STREAM)
|
|
@@ -16,14 +19,31 @@ class ConnSocket(_conn.Conn):
|
|
|
16
19
|
else:
|
|
17
20
|
host = address
|
|
18
21
|
port = 23
|
|
22
|
+
proto = "ssl" if ssl else "tcp"
|
|
19
23
|
if log:
|
|
20
|
-
log.info(f"Connecting to: {host}:{port}")
|
|
24
|
+
log.info(f"Connecting to: {host}:{port} [{proto}]")
|
|
21
25
|
try:
|
|
22
26
|
sock.connect((host, port))
|
|
23
27
|
except _socket.timeout as err:
|
|
28
|
+
sock.close()
|
|
24
29
|
raise _conn.ConnError(f"Timeout connecting to {host}:{port}") from err
|
|
25
30
|
except _socket.error as err:
|
|
31
|
+
sock.close()
|
|
26
32
|
raise _conn.ConnError(f"Cannot connect to {host}:{port}: {err}") from err
|
|
33
|
+
if ssl:
|
|
34
|
+
try:
|
|
35
|
+
context = _ssl.create_default_context()
|
|
36
|
+
if not ssl_verify:
|
|
37
|
+
context.check_hostname = False
|
|
38
|
+
context.verify_mode = _ssl.CERT_NONE
|
|
39
|
+
elif not ssl_check_hostname:
|
|
40
|
+
context.check_hostname = False
|
|
41
|
+
if ssl_ca:
|
|
42
|
+
context.load_verify_locations(ssl_ca)
|
|
43
|
+
sock = context.wrap_socket(sock, server_hostname=host)
|
|
44
|
+
except (_ssl.SSLError, OSError) as err:
|
|
45
|
+
sock.close()
|
|
46
|
+
raise _conn.ConnError(f"SSL error: {err}") from err
|
|
27
47
|
sock.settimeout(None)
|
|
28
48
|
sock.setblocking(False)
|
|
29
49
|
self._socket = sock
|
|
@@ -51,8 +71,12 @@ class ConnSocket(_conn.Conn):
|
|
|
51
71
|
if data:
|
|
52
72
|
self._log.debug("RX: %r", data)
|
|
53
73
|
return data
|
|
54
|
-
|
|
74
|
+
# recv() returned b'' = peer closed connection
|
|
75
|
+
raise _conn.ConnError("Connection closed")
|
|
76
|
+
except (BlockingIOError, _ssl.SSLWantReadError):
|
|
55
77
|
pass
|
|
78
|
+
except _conn.ConnError:
|
|
79
|
+
raise
|
|
56
80
|
except OSError as err:
|
|
57
81
|
raise _conn.ConnError(f"Connection lost: {err}") from err
|
|
58
82
|
return None
|
|
@@ -64,5 +88,7 @@ class ConnSocket(_conn.Conn):
|
|
|
64
88
|
self._log.debug("TX: %r", data)
|
|
65
89
|
try:
|
|
66
90
|
return self._socket.send(data)
|
|
91
|
+
except _ssl.SSLWantWriteError:
|
|
92
|
+
return 0
|
|
67
93
|
except OSError as err:
|
|
68
94
|
raise _conn.ConnError(f"Connection lost: {err}") from err
|
|
@@ -1329,6 +1329,29 @@ def _mt_pfind(label):
|
|
|
1329
1329
|
return True
|
|
1330
1330
|
return False
|
|
1331
1331
|
|
|
1332
|
+
def wipe(self, reconnect=True, timeout=None, on_delete=None):
|
|
1333
|
+
"""Erase all files from filesystem root, then machine-reset device
|
|
1334
|
+
|
|
1335
|
+
Deletes every entry in '/' recursively, then performs a full
|
|
1336
|
+
machine.reset() so runtime state (open sockets, WiFi, mounted VFS,
|
|
1337
|
+
etc.) is dropped too - a soft reset would leave those alive. Intended
|
|
1338
|
+
for a clean slate, e.g. at the start of integration tests.
|
|
1339
|
+
|
|
1340
|
+
Arguments:
|
|
1341
|
+
reconnect: if True, wait for the device to come back after reset
|
|
1342
|
+
timeout: reconnect timeout in seconds (None = default)
|
|
1343
|
+
on_delete: optional callback(path) invoked before each delete
|
|
1344
|
+
|
|
1345
|
+
Returns:
|
|
1346
|
+
True if reconnected after reset, False otherwise
|
|
1347
|
+
"""
|
|
1348
|
+
for name, _ in self.ls('/'):
|
|
1349
|
+
path = '/' + name
|
|
1350
|
+
if on_delete is not None:
|
|
1351
|
+
on_delete(path)
|
|
1352
|
+
self.delete(path)
|
|
1353
|
+
return self.machine_reset(reconnect=reconnect, timeout=timeout)
|
|
1354
|
+
|
|
1332
1355
|
def machine_bootloader(self):
|
|
1333
1356
|
"""Enter bootloader using machine.bootloader()
|
|
1334
1357
|
|
|
@@ -31,7 +31,7 @@ else:
|
|
|
31
31
|
# Order of commands in help and completion
|
|
32
32
|
_CMD_ORDER = [
|
|
33
33
|
'ls', 'tree', 'cat', 'cp', 'mv', 'mkdir', 'rm', 'pwd', 'cd', 'path',
|
|
34
|
-
'stop', 'reset', 'monitor', 'repl', 'exec', 'run', 'edit', 'info',
|
|
34
|
+
'stop', 'reset', 'wipe', 'monitor', 'repl', 'exec', 'run', 'edit', 'info',
|
|
35
35
|
'rtc', 'flash', 'mount', 'ln', 'speedtest', 'sleep', 'ports',
|
|
36
36
|
]
|
|
37
37
|
|
|
@@ -182,12 +182,17 @@ class MpyTool():
|
|
|
182
182
|
def __init__(
|
|
183
183
|
self, conn=None, log=None, verbose=None, exclude_dirs=None,
|
|
184
184
|
force=False, compress=None, chunk_size=None,
|
|
185
|
-
port=None, address=None, baudrate=115200
|
|
185
|
+
port=None, address=None, baudrate=115200,
|
|
186
|
+
ssl=False, ssl_verify=True, ssl_ca=None, ssl_check_hostname=True):
|
|
186
187
|
# Connection can be provided directly or created lazily from parameters
|
|
187
188
|
self._conn = conn
|
|
188
189
|
self._port = port
|
|
189
190
|
self._address = address
|
|
190
191
|
self._baudrate = baudrate
|
|
192
|
+
self._ssl = ssl
|
|
193
|
+
self._ssl_verify = ssl_verify
|
|
194
|
+
self._ssl_ca = ssl_ca
|
|
195
|
+
self._ssl_check_hostname = ssl_check_hostname
|
|
191
196
|
self._log = log if log is not None else _logger.SimpleColorLogger()
|
|
192
197
|
self._verbose_out = verbose # None = no verbose output (API mode)
|
|
193
198
|
self._exclude_dirs = {'.*', '*.pyc', '__pycache__'}
|
|
@@ -226,9 +231,12 @@ class MpyTool():
|
|
|
226
231
|
self._log.verbose(f"Connected to {port_col} {desc}", level=2)
|
|
227
232
|
elif self._address:
|
|
228
233
|
self._conn = _mpytool.ConnSocket(
|
|
229
|
-
address=self._address, log=self._log
|
|
234
|
+
address=self._address, log=self._log,
|
|
235
|
+
ssl=self._ssl, ssl_verify=self._ssl_verify,
|
|
236
|
+
ssl_ca=self._ssl_ca, ssl_check_hostname=self._ssl_check_hostname)
|
|
237
|
+
proto = "ssl" if self._ssl else "socket"
|
|
230
238
|
self._log.verbose(
|
|
231
|
-
f"Connected to {self._address} [
|
|
239
|
+
f"Connected to {self._address} [{proto}]", level=2)
|
|
232
240
|
return self._conn
|
|
233
241
|
|
|
234
242
|
@property
|
|
@@ -465,6 +473,9 @@ class MpyTool():
|
|
|
465
473
|
self.verbose("REPL (Ctrl+] to exit)", 1)
|
|
466
474
|
terminal = _terminal.Terminal(self.conn, self._log)
|
|
467
475
|
terminal.run()
|
|
476
|
+
# Drop keystrokes the device never consumed, so close() doesn't block
|
|
477
|
+
# waiting to drain the kernel TX buffer.
|
|
478
|
+
self.conn.discard_output()
|
|
468
479
|
self._log.info('Exiting..')
|
|
469
480
|
|
|
470
481
|
def cmd_ota(self, firmware_path):
|
|
@@ -759,6 +770,21 @@ class MpyTool():
|
|
|
759
770
|
except (NotImplementedError, _mpytool.ConnError) as err:
|
|
760
771
|
raise _mpytool.MpyError(f"Bootloader reset failed: {err}")
|
|
761
772
|
|
|
773
|
+
def cmd_wipe(self, reconnect=True, timeout=None):
|
|
774
|
+
"""Erase all files from root, then machine-reset the device"""
|
|
775
|
+
self.verbose("WIPE", 1)
|
|
776
|
+
try:
|
|
777
|
+
self.mpy.wipe(
|
|
778
|
+
reconnect=reconnect, timeout=timeout,
|
|
779
|
+
on_delete=lambda path: self.verbose(f" rm {path}", 1))
|
|
780
|
+
except (_mpytool.ConnError, OSError) as err:
|
|
781
|
+
self.verbose(f" reconnect failed: {err}", 1, color='red')
|
|
782
|
+
raise _mpytool.ConnError(f"Reconnect failed: {err}")
|
|
783
|
+
if reconnect:
|
|
784
|
+
self.verbose(" reset, reconnected", 1, color='green')
|
|
785
|
+
else:
|
|
786
|
+
self.verbose(" reset", 1)
|
|
787
|
+
|
|
762
788
|
@command('ls', 'List files and directories on device.')
|
|
763
789
|
@argument('path', nargs='?', default=':', metavar='remote',
|
|
764
790
|
type=_normalize_path_arg, help='device path (default: CWD)')
|
|
@@ -911,6 +937,18 @@ class MpyTool():
|
|
|
911
937
|
reconnect = has_more if mode in ('machine', 'rts') else True
|
|
912
938
|
self.cmd_reset(mode=mode, reconnect=reconnect, timeout=args.timeout)
|
|
913
939
|
|
|
940
|
+
@command('wipe', 'Erase ALL files and machine-reset the device.')
|
|
941
|
+
@option('-y', '--yes', action='store_true',
|
|
942
|
+
help='no-op (wipe never prompts), accepted for convenience')
|
|
943
|
+
@option('-t', '--timeout', type=int,
|
|
944
|
+
help='reconnect timeout in seconds')
|
|
945
|
+
def _dispatch_wipe(self, commands, is_last_group):
|
|
946
|
+
args, commands[:] = _make_parser(
|
|
947
|
+
self._dispatch_wipe).parse_known_args(commands)
|
|
948
|
+
# Reconnect only if more work follows; pointless if wipe is last.
|
|
949
|
+
has_more = bool(commands) or not is_last_group
|
|
950
|
+
self.cmd_wipe(reconnect=has_more, timeout=args.timeout)
|
|
951
|
+
|
|
914
952
|
@command('monitor', 'Monitor device output until program ends.')
|
|
915
953
|
@option('-f', '--follow', action='store_true',
|
|
916
954
|
help='follow output continuously (Ctrl-C to stop)')
|
|
@@ -1430,7 +1468,7 @@ class MpyTool():
|
|
|
1430
1468
|
|
|
1431
1469
|
_COMMANDS = frozenset({
|
|
1432
1470
|
'ls', 'tree', 'cat', 'mkdir', 'rm', 'pwd', 'cd', 'path',
|
|
1433
|
-
'reset', 'stop', 'monitor', 'repl', 'exec', 'run', 'edit', 'info',
|
|
1471
|
+
'reset', 'wipe', 'stop', 'monitor', 'repl', 'exec', 'run', 'edit', 'info',
|
|
1434
1472
|
'rtc', 'flash', 'sleep', 'cp', 'mv', 'mount', 'ln', 'speedtest',
|
|
1435
1473
|
'ports', '_paths', '_commands', '_options', '_args',
|
|
1436
1474
|
})
|
|
@@ -1547,6 +1585,16 @@ def _build_main_parser():
|
|
|
1547
1585
|
"-V", "--version", action='version', version=_VERSION_STR)
|
|
1548
1586
|
parser.add_argument('-p', '--port', help="serial port")
|
|
1549
1587
|
parser.add_argument('-a', '--address', help="network address")
|
|
1588
|
+
parser.add_argument(
|
|
1589
|
+
'--ssl', action='store_true', help='use SSL/TLS for network connection')
|
|
1590
|
+
parser.add_argument(
|
|
1591
|
+
'--ssl-no-verify', action='store_true',
|
|
1592
|
+
help='disable SSL certificate verification (insecure)')
|
|
1593
|
+
parser.add_argument(
|
|
1594
|
+
'--ssl-ca', metavar='FILE', help='CA certificate file for SSL verification')
|
|
1595
|
+
parser.add_argument(
|
|
1596
|
+
'--ssl-no-hostname', action='store_true',
|
|
1597
|
+
help='disable SSL hostname/IP verification (verify cert only)')
|
|
1550
1598
|
parser.add_argument(
|
|
1551
1599
|
'-b', '--baud', type=int, default=115200, help="baud rate")
|
|
1552
1600
|
parser.add_argument(
|
|
@@ -1600,11 +1648,17 @@ def main():
|
|
|
1600
1648
|
compress = False
|
|
1601
1649
|
elif args.compress:
|
|
1602
1650
|
compress = True
|
|
1651
|
+
# SSL: --ssl-no-verify, --ssl-ca or --ssl-no-hostname implies --ssl
|
|
1652
|
+
use_ssl = args.ssl or args.ssl_no_verify or args.ssl_ca or args.ssl_no_hostname
|
|
1653
|
+
ssl_verify = not args.ssl_no_verify
|
|
1654
|
+
ssl_check_hostname = not args.ssl_no_hostname
|
|
1603
1655
|
# Create MpyTool with lazy connection initialization
|
|
1604
1656
|
mpy_tool = MpyTool(
|
|
1605
1657
|
log=log, verbose=log, exclude_dirs=args.exclude,
|
|
1606
1658
|
force=args.force, compress=compress, chunk_size=args.chunk_size,
|
|
1607
|
-
port=args.port, address=args.address, baudrate=args.baud
|
|
1659
|
+
port=args.port, address=args.address, baudrate=args.baud,
|
|
1660
|
+
ssl=use_ssl, ssl_verify=ssl_verify, ssl_ca=args.ssl_ca,
|
|
1661
|
+
ssl_check_hostname=ssl_check_hostname)
|
|
1608
1662
|
command_groups = _utils.split_commands(args.commands)
|
|
1609
1663
|
try:
|
|
1610
1664
|
with_progress = args.verbose >= 1
|
|
@@ -16,6 +16,10 @@ class TerminalBase:
|
|
|
16
16
|
self._log = log
|
|
17
17
|
self._conn = conn
|
|
18
18
|
self._running = None
|
|
19
|
+
# Pending keystrokes to send to device. Buffered here and flushed only
|
|
20
|
+
# when the connection fd is writable, so a blocking write (e.g. device
|
|
21
|
+
# not draining USB-CDC) never stalls the loop and breaks Ctrl+] exit.
|
|
22
|
+
self._tx_buf = bytearray()
|
|
19
23
|
|
|
20
24
|
def _setup(self):
|
|
21
25
|
"""Set terminal to raw mode (platform-specific)"""
|
|
@@ -43,7 +47,17 @@ class TerminalBase:
|
|
|
43
47
|
self._running = False
|
|
44
48
|
return
|
|
45
49
|
if data:
|
|
46
|
-
self.
|
|
50
|
+
self._tx_buf += data
|
|
51
|
+
|
|
52
|
+
def _flush_tx(self):
|
|
53
|
+
"""Send buffered keystrokes to device (blocking write).
|
|
54
|
+
|
|
55
|
+
Platforms that gate this on fd writability avoid blocking; the base
|
|
56
|
+
implementation is used where writability can't be polled.
|
|
57
|
+
"""
|
|
58
|
+
if self._tx_buf:
|
|
59
|
+
self._conn.write(bytes(self._tx_buf))
|
|
60
|
+
del self._tx_buf[:]
|
|
47
61
|
|
|
48
62
|
def _read_event_device(self):
|
|
49
63
|
data = self._conn.read()
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""MicroPython tool: Unix terminal (tty/termios/select)"""
|
|
2
|
+
|
|
3
|
+
import os as _os
|
|
4
|
+
import sys as _sys
|
|
5
|
+
import select as _select
|
|
6
|
+
import tty as _tty
|
|
7
|
+
import termios as _termios
|
|
8
|
+
|
|
9
|
+
from mpytool.terminal import TerminalBase
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Terminal(TerminalBase):
|
|
13
|
+
|
|
14
|
+
def __init__(self, conn, log):
|
|
15
|
+
super().__init__(conn, log)
|
|
16
|
+
self._stdin_fd = _sys.stdin.fileno()
|
|
17
|
+
self._orig_attr = _termios.tcgetattr(self._stdin_fd)
|
|
18
|
+
|
|
19
|
+
def __del__(self):
|
|
20
|
+
if self._orig_attr:
|
|
21
|
+
_termios.tcsetattr(
|
|
22
|
+
self._stdin_fd, _termios.TCSANOW, self._orig_attr)
|
|
23
|
+
|
|
24
|
+
def _setup(self):
|
|
25
|
+
_tty.setraw(self._stdin_fd)
|
|
26
|
+
|
|
27
|
+
def _restore(self):
|
|
28
|
+
if self._orig_attr:
|
|
29
|
+
_termios.tcsetattr(
|
|
30
|
+
self._stdin_fd, _termios.TCSANOW, self._orig_attr)
|
|
31
|
+
self._orig_attr = None
|
|
32
|
+
|
|
33
|
+
def read(self):
|
|
34
|
+
return _sys.stdin.buffer.raw.read(1)
|
|
35
|
+
|
|
36
|
+
def _flush_tx(self):
|
|
37
|
+
"""Write a partial chunk without blocking.
|
|
38
|
+
|
|
39
|
+
Called only when the connection fd is writable, so os.write() writes
|
|
40
|
+
as much as fits and returns immediately; the rest stays buffered for
|
|
41
|
+
the next writable event.
|
|
42
|
+
"""
|
|
43
|
+
if not self._tx_buf:
|
|
44
|
+
return
|
|
45
|
+
try:
|
|
46
|
+
count = _os.write(self._conn.fd, self._tx_buf)
|
|
47
|
+
except BlockingIOError:
|
|
48
|
+
return
|
|
49
|
+
if count:
|
|
50
|
+
self._log.debug('TX: %r', bytes(self._tx_buf[:count]))
|
|
51
|
+
del self._tx_buf[:count]
|
|
52
|
+
|
|
53
|
+
def _loop(self):
|
|
54
|
+
rd_fds = [self._stdin_fd, self._conn.fd]
|
|
55
|
+
while self._running:
|
|
56
|
+
# Only watch for writability while there is something to send,
|
|
57
|
+
# otherwise select() would busy-loop on the always-writable fd.
|
|
58
|
+
wr_fds = [self._conn.fd] if self._tx_buf else []
|
|
59
|
+
readable, writable, _ = _select.select(rd_fds, wr_fds, [], 1)
|
|
60
|
+
if self._stdin_fd in readable:
|
|
61
|
+
self._read_event_terminal()
|
|
62
|
+
if self._conn.fd in readable:
|
|
63
|
+
self._read_event_device()
|
|
64
|
+
if self._conn.fd in writable:
|
|
65
|
+
self._flush_tx()
|