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.
Files changed (50) hide show
  1. {mpytool-2.3.6 → mpytool-2.4.0}/PKG-INFO +34 -4
  2. {mpytool-2.3.6 → mpytool-2.4.0}/README.md +33 -3
  3. {mpytool-2.3.6 → mpytool-2.4.0}/README_API.md +53 -5
  4. {mpytool-2.3.6 → mpytool-2.4.0}/completions/_mpytool +4 -0
  5. {mpytool-2.3.6 → mpytool-2.4.0}/completions/mpytool.bash +4 -0
  6. mpytool-2.4.0/mpytool/_version.py +24 -0
  7. {mpytool-2.3.6 → mpytool-2.4.0}/mpytool/conn.py +7 -0
  8. {mpytool-2.3.6 → mpytool-2.4.0}/mpytool/conn_serial.py +13 -0
  9. {mpytool-2.3.6 → mpytool-2.4.0}/mpytool/conn_socket.py +29 -3
  10. {mpytool-2.3.6 → mpytool-2.4.0}/mpytool/mpy.py +23 -0
  11. {mpytool-2.3.6 → mpytool-2.4.0}/mpytool/mpytool.py +60 -6
  12. {mpytool-2.3.6 → mpytool-2.4.0}/mpytool/terminal.py +15 -1
  13. mpytool-2.4.0/mpytool/terminal_unix.py +65 -0
  14. {mpytool-2.3.6 → mpytool-2.4.0}/mpytool/terminal_win.py +3 -0
  15. {mpytool-2.3.6 → mpytool-2.4.0}/mpytool.egg-info/PKG-INFO +34 -4
  16. {mpytool-2.3.6 → mpytool-2.4.0}/mpytool.egg-info/SOURCES.txt +1 -0
  17. {mpytool-2.3.6 → mpytool-2.4.0}/tests/test_cli.py +14 -3
  18. mpytool-2.4.0/tests/test_conn_socket.py +307 -0
  19. {mpytool-2.3.6 → mpytool-2.4.0}/tests/test_integration.py +4 -2
  20. {mpytool-2.3.6 → mpytool-2.4.0}/tests/test_mpy.py +55 -0
  21. mpytool-2.3.6/mpytool/_version.py +0 -34
  22. mpytool-2.3.6/mpytool/terminal_unix.py +0 -43
  23. {mpytool-2.3.6 → mpytool-2.4.0}/.github/workflows/integration.yml +0 -0
  24. {mpytool-2.3.6 → mpytool-2.4.0}/.github/workflows/publish.yml +0 -0
  25. {mpytool-2.3.6 → mpytool-2.4.0}/.github/workflows/tests.yml +0 -0
  26. {mpytool-2.3.6 → mpytool-2.4.0}/.gitignore +0 -0
  27. {mpytool-2.3.6 → mpytool-2.4.0}/LICENSE +0 -0
  28. {mpytool-2.3.6 → mpytool-2.4.0}/README_bench.md +0 -0
  29. {mpytool-2.3.6 → mpytool-2.4.0}/README_mpremote.md +0 -0
  30. {mpytool-2.3.6 → mpytool-2.4.0}/mpytool/__init__.py +0 -0
  31. {mpytool-2.3.6 → mpytool-2.4.0}/mpytool/cmd_cp.py +0 -0
  32. {mpytool-2.3.6 → mpytool-2.4.0}/mpytool/logger.py +0 -0
  33. {mpytool-2.3.6 → mpytool-2.4.0}/mpytool/mount.py +0 -0
  34. {mpytool-2.3.6 → mpytool-2.4.0}/mpytool/mpy_comm.py +0 -0
  35. {mpytool-2.3.6 → mpytool-2.4.0}/mpytool/mpy_cross.py +0 -0
  36. {mpytool-2.3.6 → mpytool-2.4.0}/mpytool/speedtest.py +0 -0
  37. {mpytool-2.3.6 → mpytool-2.4.0}/mpytool/utils.py +0 -0
  38. {mpytool-2.3.6 → mpytool-2.4.0}/mpytool.egg-info/dependency_links.txt +0 -0
  39. {mpytool-2.3.6 → mpytool-2.4.0}/mpytool.egg-info/entry_points.txt +0 -0
  40. {mpytool-2.3.6 → mpytool-2.4.0}/mpytool.egg-info/requires.txt +0 -0
  41. {mpytool-2.3.6 → mpytool-2.4.0}/mpytool.egg-info/top_level.txt +0 -0
  42. {mpytool-2.3.6 → mpytool-2.4.0}/pyproject.toml +0 -0
  43. {mpytool-2.3.6 → mpytool-2.4.0}/setup.cfg +0 -0
  44. {mpytool-2.3.6 → mpytool-2.4.0}/tests/__init__.py +0 -0
  45. {mpytool-2.3.6 → mpytool-2.4.0}/tests/test_cmd_cp.py +0 -0
  46. {mpytool-2.3.6 → mpytool-2.4.0}/tests/test_errors.py +0 -0
  47. {mpytool-2.3.6 → mpytool-2.4.0}/tests/test_helpers.py +0 -0
  48. {mpytool-2.3.6 → mpytool-2.4.0}/tests/test_mount.py +0 -0
  49. {mpytool-2.3.6 → mpytool-2.4.0}/tests/test_mpytool.py +0 -0
  50. {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.6
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
- **Example:**
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]' \
@@ -170,6 +170,10 @@ _mpytool() {
170
170
  -a|--address)
171
171
  return
172
172
  ;;
173
+ --ssl-ca)
174
+ COMPREPLY=($(compgen -f -- "$cur"))
175
+ return
176
+ ;;
173
177
  esac
174
178
 
175
179
  # At command position
@@ -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__(self, address, log=None):
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
- except BlockingIOError:
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} [socket]", level=2)
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._conn.write(data)
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()
@@ -81,6 +81,9 @@ class Terminal(TerminalBase):
81
81
  if _msvcrt.kbhit():
82
82
  self._read_event_terminal()
83
83
  activity = True
84
+ if self._tx_buf:
85
+ self._flush_tx()
86
+ activity = True
84
87
  if self._conn._has_data(0):
85
88
  self._read_event_device()
86
89
  activity = True