mpytool 2.3.6__tar.gz → 2.5.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 (52) hide show
  1. {mpytool-2.3.6 → mpytool-2.5.0}/PKG-INFO +78 -4
  2. {mpytool-2.3.6 → mpytool-2.5.0}/README.md +77 -3
  3. {mpytool-2.3.6 → mpytool-2.5.0}/README_API.md +53 -5
  4. {mpytool-2.3.6 → mpytool-2.5.0}/completions/_mpytool +11 -0
  5. {mpytool-2.3.6 → mpytool-2.5.0}/completions/mpytool.bash +11 -0
  6. mpytool-2.5.0/mpytool/_version.py +24 -0
  7. {mpytool-2.3.6 → mpytool-2.5.0}/mpytool/conn.py +7 -0
  8. {mpytool-2.3.6 → mpytool-2.5.0}/mpytool/conn_serial.py +13 -0
  9. {mpytool-2.3.6 → mpytool-2.5.0}/mpytool/conn_socket.py +29 -3
  10. {mpytool-2.3.6 → mpytool-2.5.0}/mpytool/mpy.py +23 -0
  11. {mpytool-2.3.6 → mpytool-2.5.0}/mpytool/mpytool.py +185 -8
  12. {mpytool-2.3.6 → mpytool-2.5.0}/mpytool/terminal.py +15 -1
  13. mpytool-2.5.0/mpytool/terminal_unix.py +65 -0
  14. {mpytool-2.3.6 → mpytool-2.5.0}/mpytool/terminal_win.py +3 -0
  15. {mpytool-2.3.6 → mpytool-2.5.0}/mpytool.egg-info/PKG-INFO +78 -4
  16. {mpytool-2.3.6 → mpytool-2.5.0}/mpytool.egg-info/SOURCES.txt +3 -0
  17. mpytool-2.5.0/mpytool.egg-info/scm_file_list.json +43 -0
  18. mpytool-2.5.0/mpytool.egg-info/scm_version.json +8 -0
  19. {mpytool-2.3.6 → mpytool-2.5.0}/tests/test_cli.py +27 -3
  20. mpytool-2.5.0/tests/test_conn_socket.py +307 -0
  21. {mpytool-2.3.6 → mpytool-2.5.0}/tests/test_integration.py +4 -2
  22. {mpytool-2.3.6 → mpytool-2.5.0}/tests/test_mpy.py +55 -0
  23. {mpytool-2.3.6 → mpytool-2.5.0}/tests/test_mpytool.py +132 -0
  24. mpytool-2.3.6/mpytool/_version.py +0 -34
  25. mpytool-2.3.6/mpytool/terminal_unix.py +0 -43
  26. {mpytool-2.3.6 → mpytool-2.5.0}/.github/workflows/integration.yml +0 -0
  27. {mpytool-2.3.6 → mpytool-2.5.0}/.github/workflows/publish.yml +0 -0
  28. {mpytool-2.3.6 → mpytool-2.5.0}/.github/workflows/tests.yml +0 -0
  29. {mpytool-2.3.6 → mpytool-2.5.0}/.gitignore +0 -0
  30. {mpytool-2.3.6 → mpytool-2.5.0}/LICENSE +0 -0
  31. {mpytool-2.3.6 → mpytool-2.5.0}/README_bench.md +0 -0
  32. {mpytool-2.3.6 → mpytool-2.5.0}/README_mpremote.md +0 -0
  33. {mpytool-2.3.6 → mpytool-2.5.0}/mpytool/__init__.py +0 -0
  34. {mpytool-2.3.6 → mpytool-2.5.0}/mpytool/cmd_cp.py +0 -0
  35. {mpytool-2.3.6 → mpytool-2.5.0}/mpytool/logger.py +0 -0
  36. {mpytool-2.3.6 → mpytool-2.5.0}/mpytool/mount.py +0 -0
  37. {mpytool-2.3.6 → mpytool-2.5.0}/mpytool/mpy_comm.py +0 -0
  38. {mpytool-2.3.6 → mpytool-2.5.0}/mpytool/mpy_cross.py +0 -0
  39. {mpytool-2.3.6 → mpytool-2.5.0}/mpytool/speedtest.py +0 -0
  40. {mpytool-2.3.6 → mpytool-2.5.0}/mpytool/utils.py +0 -0
  41. {mpytool-2.3.6 → mpytool-2.5.0}/mpytool.egg-info/dependency_links.txt +0 -0
  42. {mpytool-2.3.6 → mpytool-2.5.0}/mpytool.egg-info/entry_points.txt +0 -0
  43. {mpytool-2.3.6 → mpytool-2.5.0}/mpytool.egg-info/requires.txt +0 -0
  44. {mpytool-2.3.6 → mpytool-2.5.0}/mpytool.egg-info/top_level.txt +0 -0
  45. {mpytool-2.3.6 → mpytool-2.5.0}/pyproject.toml +0 -0
  46. {mpytool-2.3.6 → mpytool-2.5.0}/setup.cfg +0 -0
  47. {mpytool-2.3.6 → mpytool-2.5.0}/tests/__init__.py +0 -0
  48. {mpytool-2.3.6 → mpytool-2.5.0}/tests/test_cmd_cp.py +0 -0
  49. {mpytool-2.3.6 → mpytool-2.5.0}/tests/test_errors.py +0 -0
  50. {mpytool-2.3.6 → mpytool-2.5.0}/tests/test_helpers.py +0 -0
  51. {mpytool-2.3.6 → mpytool-2.5.0}/tests/test_mount.py +0 -0
  52. {mpytool-2.3.6 → mpytool-2.5.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.5.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,11 @@ 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)
36
+ - **Deploy projects** - `sync` uploads a whole project to the device
37
+ using a `.mpyproject` config file (deploy map, excludes, `.mpy`
38
+ compilation)
34
39
  - **General-purpose serial terminal** - `repl` and `monitor` work
35
40
  with any serial device
36
41
  - **Mount local directory** - VFS mount (read-only or read-write)
@@ -42,7 +47,7 @@ It is an alternative to the official [mpremote](https://docs.micropython.org/en/
42
47
  - **Raw-paste mode** - flow-controlled code execution with reduced
43
48
  RAM usage (API)
44
49
  - **Shell completion** - ZSH and Bash with remote path completion
45
- - **Network support** - connect over TCP
50
+ - **Network support** - connect over TCP with optional SSL/TLS
46
51
  - **Full Unicode support** - handles Unicode in file names and content
47
52
 
48
53
  See [README_mpremote.md](README_mpremote.md) for detailed comparison with mpremote.
@@ -166,6 +171,47 @@ Requires `mpy-cross` in PATH (`pip install mpy-cross`). If mpy-cross version dif
166
171
  from device, automatically uses `-b` flag to target the correct bytecode version.
167
172
  If `mpy-cross` is not found, falls back to uploading `.py` with a warning.
168
173
 
174
+ ### Deploy project (sync)
175
+
176
+ `sync` uploads a whole project to the device based on a `.mpyproject`
177
+ configuration file. It is the command-line counterpart of the deploy feature in
178
+ the [Sublime Text plugin](https://github.com/cortexm/mpytool-sublime) and shares
179
+ the same `.mpyproject` format. Unlike the plugin it is non-interactive: when more
180
+ than one device is connected, pass `-p` to pick the port.
181
+
182
+ ```
183
+ $ mpytool sync # deploy project in CWD (.mpyproject searched upward)
184
+ $ mpytool sync ~/projects/blink # deploy a project in another directory
185
+ $ mpytool -p /dev/ttyACM0 sync # pick port explicitly (multiple devices)
186
+ $ mpytool sync -- reset -- monitor # deploy, then reset and watch output
187
+ ```
188
+
189
+ The `.mpyproject` file (JSON, trailing commas tolerated) describes what to upload:
190
+
191
+ ```json
192
+ {
193
+ "name": "blink",
194
+ "compile": true,
195
+ "deploy": {
196
+ "": ["./"],
197
+ "/lib/": ["../shared/utils.py"],
198
+ "/main.py": "src/main_dev.py"
199
+ },
200
+ "exclude": ["*.bak", "tests"]
201
+ }
202
+ ```
203
+
204
+ - `deploy` maps a device destination to its source(s), relative to the project
205
+ root. A key of `""` or one ending in `/` is a **directory** (value is a list of
206
+ sources); any other key is a **rename** (value is a single source file).
207
+ Defaults to `{"": ["./"]}` (whole project to root) when omitted.
208
+ - `compile: true` compiles `.py` to `.mpy` (same as `cp -m`).
209
+ - `exclude` adds ignore patterns on top of the defaults (`__pycache__`, `*.pyc`,
210
+ dotfiles). Combined with global `-e` patterns.
211
+
212
+ The connection fields (`port`, `address`) used by the Sublime plugin are ignored
213
+ by the CLI — use the global `-p` / `-a` options instead.
214
+
169
215
  ### Move/rename on device
170
216
  ```
171
217
  $ mpytool mv :/old.py :/new.py # rename file
@@ -270,6 +316,30 @@ $ mpytool sleep 2 # sleep for 2 seconds (useful between commands)
270
316
  SLEEP 2.0s
271
317
  ```
272
318
 
319
+ ### Wipe device (erase all files + machine reset)
320
+ ```
321
+ $ mpytool wipe # delete everything from / then machine.reset()
322
+ WIPE
323
+ rm /boot.py
324
+ rm /main.py
325
+ rm /lib
326
+ reset
327
+
328
+ $ mpytool wipe -- ls # wipe, reconnect, then list (now empty)
329
+ WIPE
330
+ rm /main.py
331
+ reset, reconnected
332
+ LS:
333
+ ```
334
+
335
+ `wipe` erases the whole filesystem (recursive delete of `/`) and then does a
336
+ full `machine.reset()` — a soft reset would leave open sockets, WiFi and
337
+ mounted VFS alive, so machine reset is used for a true clean slate. It **never
338
+ prompts** (mpytool has no interactive mode); `-y`/`--yes` is accepted as a
339
+ no-op. When `wipe` is the last command it just resets and exits; when more
340
+ commands follow it reconnects first (use `-t SECONDS` for the reconnect
341
+ timeout). See [README_API.md](README_API.md) for `Mpy.wipe()`.
342
+
273
343
  ### Serial terminal and monitor (general purpose)
274
344
  ```
275
345
  $ mpytool repl # auto-detect port, 115200 baud
@@ -537,8 +607,12 @@ TREE: /
537
607
 
538
608
  ### Connect over network (TCP, default port 23)
539
609
  ```
540
- $ mpytool -a 192.168.1.100 ls
541
- $ mpytool -a 192.168.1.100:8266 tree
610
+ $ mpytool -a 192.168.1.100 ls # plain TCP
611
+ $ mpytool -a 192.168.1.100:8266 tree # custom port
612
+ $ mpytool -a 192.168.1.100:8023 --ssl ls # SSL/TLS connection
613
+ $ mpytool -a 192.168.1.100:8023 --ssl-ca ca.pem ls # SSL with custom CA
614
+ $ mpytool -a 192.168.1.100:8023 --ssl-no-hostname ls # SSL, skip hostname check
615
+ $ mpytool -a 192.168.1.100:8023 --ssl-no-verify ls # SSL, skip all verification
542
616
  ```
543
617
 
544
618
  ### Set baudrate (default 115200)
@@ -16,6 +16,11 @@ 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)
21
+ - **Deploy projects** - `sync` uploads a whole project to the device
22
+ using a `.mpyproject` config file (deploy map, excludes, `.mpy`
23
+ compilation)
19
24
  - **General-purpose serial terminal** - `repl` and `monitor` work
20
25
  with any serial device
21
26
  - **Mount local directory** - VFS mount (read-only or read-write)
@@ -27,7 +32,7 @@ It is an alternative to the official [mpremote](https://docs.micropython.org/en/
27
32
  - **Raw-paste mode** - flow-controlled code execution with reduced
28
33
  RAM usage (API)
29
34
  - **Shell completion** - ZSH and Bash with remote path completion
30
- - **Network support** - connect over TCP
35
+ - **Network support** - connect over TCP with optional SSL/TLS
31
36
  - **Full Unicode support** - handles Unicode in file names and content
32
37
 
33
38
  See [README_mpremote.md](README_mpremote.md) for detailed comparison with mpremote.
@@ -151,6 +156,47 @@ Requires `mpy-cross` in PATH (`pip install mpy-cross`). If mpy-cross version dif
151
156
  from device, automatically uses `-b` flag to target the correct bytecode version.
152
157
  If `mpy-cross` is not found, falls back to uploading `.py` with a warning.
153
158
 
159
+ ### Deploy project (sync)
160
+
161
+ `sync` uploads a whole project to the device based on a `.mpyproject`
162
+ configuration file. It is the command-line counterpart of the deploy feature in
163
+ the [Sublime Text plugin](https://github.com/cortexm/mpytool-sublime) and shares
164
+ the same `.mpyproject` format. Unlike the plugin it is non-interactive: when more
165
+ than one device is connected, pass `-p` to pick the port.
166
+
167
+ ```
168
+ $ mpytool sync # deploy project in CWD (.mpyproject searched upward)
169
+ $ mpytool sync ~/projects/blink # deploy a project in another directory
170
+ $ mpytool -p /dev/ttyACM0 sync # pick port explicitly (multiple devices)
171
+ $ mpytool sync -- reset -- monitor # deploy, then reset and watch output
172
+ ```
173
+
174
+ The `.mpyproject` file (JSON, trailing commas tolerated) describes what to upload:
175
+
176
+ ```json
177
+ {
178
+ "name": "blink",
179
+ "compile": true,
180
+ "deploy": {
181
+ "": ["./"],
182
+ "/lib/": ["../shared/utils.py"],
183
+ "/main.py": "src/main_dev.py"
184
+ },
185
+ "exclude": ["*.bak", "tests"]
186
+ }
187
+ ```
188
+
189
+ - `deploy` maps a device destination to its source(s), relative to the project
190
+ root. A key of `""` or one ending in `/` is a **directory** (value is a list of
191
+ sources); any other key is a **rename** (value is a single source file).
192
+ Defaults to `{"": ["./"]}` (whole project to root) when omitted.
193
+ - `compile: true` compiles `.py` to `.mpy` (same as `cp -m`).
194
+ - `exclude` adds ignore patterns on top of the defaults (`__pycache__`, `*.pyc`,
195
+ dotfiles). Combined with global `-e` patterns.
196
+
197
+ The connection fields (`port`, `address`) used by the Sublime plugin are ignored
198
+ by the CLI — use the global `-p` / `-a` options instead.
199
+
154
200
  ### Move/rename on device
155
201
  ```
156
202
  $ mpytool mv :/old.py :/new.py # rename file
@@ -255,6 +301,30 @@ $ mpytool sleep 2 # sleep for 2 seconds (useful between commands)
255
301
  SLEEP 2.0s
256
302
  ```
257
303
 
304
+ ### Wipe device (erase all files + machine reset)
305
+ ```
306
+ $ mpytool wipe # delete everything from / then machine.reset()
307
+ WIPE
308
+ rm /boot.py
309
+ rm /main.py
310
+ rm /lib
311
+ reset
312
+
313
+ $ mpytool wipe -- ls # wipe, reconnect, then list (now empty)
314
+ WIPE
315
+ rm /main.py
316
+ reset, reconnected
317
+ LS:
318
+ ```
319
+
320
+ `wipe` erases the whole filesystem (recursive delete of `/`) and then does a
321
+ full `machine.reset()` — a soft reset would leave open sockets, WiFi and
322
+ mounted VFS alive, so machine reset is used for a true clean slate. It **never
323
+ prompts** (mpytool has no interactive mode); `-y`/`--yes` is accepted as a
324
+ no-op. When `wipe` is the last command it just resets and exits; when more
325
+ commands follow it reconnects first (use `-t SECONDS` for the reconnect
326
+ timeout). See [README_API.md](README_API.md) for `Mpy.wipe()`.
327
+
258
328
  ### Serial terminal and monitor (general purpose)
259
329
  ```
260
330
  $ mpytool repl # auto-detect port, 115200 baud
@@ -522,8 +592,12 @@ TREE: /
522
592
 
523
593
  ### Connect over network (TCP, default port 23)
524
594
  ```
525
- $ mpytool -a 192.168.1.100 ls
526
- $ mpytool -a 192.168.1.100:8266 tree
595
+ $ mpytool -a 192.168.1.100 ls # plain TCP
596
+ $ mpytool -a 192.168.1.100:8266 tree # custom port
597
+ $ mpytool -a 192.168.1.100:8023 --ssl ls # SSL/TLS connection
598
+ $ mpytool -a 192.168.1.100:8023 --ssl-ca ca.pem ls # SSL with custom CA
599
+ $ mpytool -a 192.168.1.100:8023 --ssl-no-hostname ls # SSL, skip hostname check
600
+ $ mpytool -a 192.168.1.100:8023 --ssl-no-verify ls # SSL, skip all verification
527
601
  ```
528
602
 
529
603
  ### 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]' \
@@ -265,6 +269,13 @@ _mpytool() {
265
269
  fi
266
270
  fi
267
271
  ;;
272
+ sync)
273
+ # Optional local project directory, -- anytime
274
+ if [[ $pos -eq 2 ]]; then
275
+ _files -/
276
+ fi
277
+ compadd -- '--'
278
+ ;;
268
279
  exec)
269
280
  # 1 code string, -- after it
270
281
  [[ $nargs -ge 1 ]] && compadd -- '--'
@@ -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
@@ -268,6 +272,13 @@ _mpytool() {
268
272
  fi
269
273
  fi
270
274
  ;;
275
+ sync)
276
+ # Optional local project directory, -- for chaining
277
+ if [[ $pos -eq 2 ]]; then
278
+ COMPREPLY=($(compgen -d -- "$cur"))
279
+ fi
280
+ [[ -z "$cur" || "--" == "$cur"* ]] && COMPREPLY+=("--")
281
+ ;;
271
282
  exec)
272
283
  # 1 code string, -- after it
273
284
  [[ $nargs -ge 1 && ( -z "$cur" || "--" == "$cur"* ) ]] && COMPREPLY+=("--")
@@ -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.5.0'
22
+ __version_tuple__ = version_tuple = (2, 5, 0)
23
+
24
+ __commit_id__ = commit_id = 'ga70fd9bfe'
@@ -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