mpytool 2.2.0__tar.gz → 2.2.2__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.2.0 → mpytool-2.2.2}/PKG-INFO +10 -2
- {mpytool-2.2.0 → mpytool-2.2.2}/README.md +9 -1
- {mpytool-2.2.0 → mpytool-2.2.2}/mpytool/conn.py +3 -0
- {mpytool-2.2.0 → mpytool-2.2.2}/mpytool/conn_serial.py +23 -1
- {mpytool-2.2.0 → mpytool-2.2.2}/mpytool/conn_socket.py +5 -1
- {mpytool-2.2.0 → mpytool-2.2.2}/mpytool/logger.py +22 -9
- {mpytool-2.2.0 → mpytool-2.2.2}/mpytool/mpy_comm.py +29 -0
- {mpytool-2.2.0 → mpytool-2.2.2}/mpytool/mpytool.py +65 -34
- mpytool-2.2.2/mpytool/terminal.py +80 -0
- mpytool-2.2.2/mpytool/terminal_unix.py +45 -0
- mpytool-2.2.2/mpytool/terminal_win.py +88 -0
- {mpytool-2.2.0 → mpytool-2.2.2}/mpytool/utils.py +5 -1
- {mpytool-2.2.0 → mpytool-2.2.2}/mpytool.egg-info/PKG-INFO +10 -2
- {mpytool-2.2.0 → mpytool-2.2.2}/mpytool.egg-info/SOURCES.txt +2 -0
- {mpytool-2.2.0 → mpytool-2.2.2}/pyproject.toml +1 -1
- {mpytool-2.2.0 → mpytool-2.2.2}/tests/test_errors.py +31 -0
- {mpytool-2.2.0 → mpytool-2.2.2}/tests/test_integration.py +22 -1
- {mpytool-2.2.0 → mpytool-2.2.2}/tests/test_utils.py +6 -4
- mpytool-2.2.0/mpytool/terminal.py +0 -76
- {mpytool-2.2.0 → mpytool-2.2.2}/LICENSE +0 -0
- {mpytool-2.2.0 → mpytool-2.2.2}/mpytool/__init__.py +0 -0
- {mpytool-2.2.0 → mpytool-2.2.2}/mpytool/mpy.py +0 -0
- {mpytool-2.2.0 → mpytool-2.2.2}/mpytool.egg-info/dependency_links.txt +0 -0
- {mpytool-2.2.0 → mpytool-2.2.2}/mpytool.egg-info/entry_points.txt +0 -0
- {mpytool-2.2.0 → mpytool-2.2.2}/mpytool.egg-info/requires.txt +0 -0
- {mpytool-2.2.0 → mpytool-2.2.2}/mpytool.egg-info/top_level.txt +0 -0
- {mpytool-2.2.0 → mpytool-2.2.2}/setup.cfg +0 -0
- {mpytool-2.2.0 → mpytool-2.2.2}/tests/test_mpy.py +0 -0
- {mpytool-2.2.0 → mpytool-2.2.2}/tests/test_mpytool.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mpytool
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.2
|
|
4
4
|
Summary: MPY tool - manage files on devices running MicroPython
|
|
5
5
|
Author-email: Pavel Revak <pavel.revak@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -446,7 +446,15 @@ Working only with MicroPython boards, not with CircuitPython
|
|
|
446
446
|
|
|
447
447
|
- Linux
|
|
448
448
|
- MacOS
|
|
449
|
-
- Windows
|
|
449
|
+
- Windows
|
|
450
|
+
|
|
451
|
+
### Windows notes
|
|
452
|
+
|
|
453
|
+
All commands work on Windows including `repl`.
|
|
454
|
+
|
|
455
|
+
**CMD.EXE**: ANSI colors are disabled, progress indicator works. Log messages use text prefixes (`E:`, `W:`, `I:`, `D:`).
|
|
456
|
+
|
|
457
|
+
**Git Bash**: Full color support (sets `TERM` environment variable).
|
|
450
458
|
|
|
451
459
|
## Credits
|
|
452
460
|
|
|
@@ -432,7 +432,15 @@ Working only with MicroPython boards, not with CircuitPython
|
|
|
432
432
|
|
|
433
433
|
- Linux
|
|
434
434
|
- MacOS
|
|
435
|
-
- Windows
|
|
435
|
+
- Windows
|
|
436
|
+
|
|
437
|
+
### Windows notes
|
|
438
|
+
|
|
439
|
+
All commands work on Windows including `repl`.
|
|
440
|
+
|
|
441
|
+
**CMD.EXE**: ANSI colors are disabled, progress indicator works. Log messages use text prefixes (`E:`, `W:`, `I:`, `D:`).
|
|
442
|
+
|
|
443
|
+
**Git Bash**: Full color support (sets `TERM` environment variable).
|
|
436
444
|
|
|
437
445
|
## Credits
|
|
438
446
|
|
|
@@ -116,6 +116,9 @@ class Conn():
|
|
|
116
116
|
line = self.read_until(b'\n', timeout)
|
|
117
117
|
return line.strip(b'\r')
|
|
118
118
|
|
|
119
|
+
def close(self):
|
|
120
|
+
"""Close connection"""
|
|
121
|
+
|
|
119
122
|
def hard_reset(self):
|
|
120
123
|
"""Hardware reset (only available on serial connections)"""
|
|
121
124
|
raise NotImplementedError("Hardware reset not available on this connection")
|
|
@@ -17,15 +17,37 @@ class ConnSerial(_conn.Conn):
|
|
|
17
17
|
self._serial = None
|
|
18
18
|
raise _conn.ConnError(
|
|
19
19
|
f"Error opening serial port {serial_config['port']}") from err
|
|
20
|
+
# Windows: pyserial has no fd, select() works only with sockets
|
|
21
|
+
if not hasattr(self._serial, 'fd'):
|
|
22
|
+
self._has_data = self._has_data_polling
|
|
20
23
|
|
|
21
|
-
def
|
|
24
|
+
def close(self):
|
|
22
25
|
if self._serial:
|
|
23
26
|
self._serial.close()
|
|
27
|
+
self._serial = None
|
|
28
|
+
|
|
29
|
+
def __del__(self):
|
|
30
|
+
self.close()
|
|
24
31
|
|
|
25
32
|
@property
|
|
26
33
|
def fd(self):
|
|
27
34
|
return self._serial.fd if self._serial else None
|
|
28
35
|
|
|
36
|
+
def _has_data_polling(self, timeout=0):
|
|
37
|
+
"""Check if data is available using in_waiting (Windows)"""
|
|
38
|
+
try:
|
|
39
|
+
if self._serial.in_waiting > 0:
|
|
40
|
+
return True
|
|
41
|
+
if timeout > 0:
|
|
42
|
+
deadline = _time.time() + timeout
|
|
43
|
+
while _time.time() < deadline:
|
|
44
|
+
if self._serial.in_waiting > 0:
|
|
45
|
+
return True
|
|
46
|
+
_time.sleep(0.001)
|
|
47
|
+
return False
|
|
48
|
+
except OSError:
|
|
49
|
+
return False
|
|
50
|
+
|
|
29
51
|
def _read_available(self):
|
|
30
52
|
"""Read available data from serial port"""
|
|
31
53
|
if self._serial is None:
|
|
@@ -30,9 +30,13 @@ class ConnSocket(_conn.Conn):
|
|
|
30
30
|
if log:
|
|
31
31
|
log.info("connected")
|
|
32
32
|
|
|
33
|
-
def
|
|
33
|
+
def close(self):
|
|
34
34
|
if self._socket:
|
|
35
35
|
self._socket.close()
|
|
36
|
+
self._socket = None
|
|
37
|
+
|
|
38
|
+
def __del__(self):
|
|
39
|
+
self.close()
|
|
36
40
|
|
|
37
41
|
@property
|
|
38
42
|
def fd(self):
|
|
@@ -28,21 +28,31 @@ class SimpleColorLogger():
|
|
|
28
28
|
def __init__(self, loglevel=1, verbose_level=0):
|
|
29
29
|
self._loglevel = loglevel
|
|
30
30
|
self._verbose_level = verbose_level
|
|
31
|
-
self._is_tty = (
|
|
32
|
-
|
|
31
|
+
self._is_tty = _sys.stderr.isatty()
|
|
32
|
+
self._color = (
|
|
33
|
+
self._is_tty
|
|
33
34
|
and _os.environ.get('NO_COLOR') is None
|
|
34
35
|
and _os.environ.get('TERM') != 'dumb'
|
|
35
36
|
and _os.environ.get('CI') is None
|
|
37
|
+
and (_sys.platform != 'win32' or _os.environ.get('TERM'))
|
|
36
38
|
)
|
|
39
|
+
self._pending_line = False # True when last output had no trailing newline
|
|
40
|
+
|
|
41
|
+
def _clear_pending(self):
|
|
42
|
+
"""End pending progress line before printing a new message"""
|
|
43
|
+
if self._pending_line:
|
|
44
|
+
print(file=_sys.stderr)
|
|
45
|
+
self._pending_line = False
|
|
37
46
|
|
|
38
47
|
def log(self, msg):
|
|
48
|
+
self._clear_pending()
|
|
39
49
|
print(msg, file=_sys.stderr)
|
|
40
50
|
|
|
41
51
|
def error(self, msg, *args):
|
|
42
52
|
if args:
|
|
43
53
|
msg = msg % args
|
|
44
54
|
if self._loglevel >= 1:
|
|
45
|
-
if self.
|
|
55
|
+
if self._color:
|
|
46
56
|
self.log(f"{self._BOLD_RED}{msg}{self._RESET}")
|
|
47
57
|
else:
|
|
48
58
|
self.log(f"E: {msg}")
|
|
@@ -51,7 +61,7 @@ class SimpleColorLogger():
|
|
|
51
61
|
if args:
|
|
52
62
|
msg = msg % args
|
|
53
63
|
if self._loglevel >= 2:
|
|
54
|
-
if self.
|
|
64
|
+
if self._color:
|
|
55
65
|
self.log(f"{self._BOLD_YELLOW}{msg}{self._RESET}")
|
|
56
66
|
else:
|
|
57
67
|
self.log(f"W: {msg}")
|
|
@@ -60,7 +70,7 @@ class SimpleColorLogger():
|
|
|
60
70
|
if args:
|
|
61
71
|
msg = msg % args
|
|
62
72
|
if self._loglevel >= 3:
|
|
63
|
-
if self.
|
|
73
|
+
if self._color:
|
|
64
74
|
self.log(f"{self._BOLD_MAGENTA}{msg}{self._RESET}")
|
|
65
75
|
else:
|
|
66
76
|
self.log(f"I: {msg}")
|
|
@@ -69,7 +79,7 @@ class SimpleColorLogger():
|
|
|
69
79
|
if args:
|
|
70
80
|
msg = msg % args
|
|
71
81
|
if self._loglevel >= 4:
|
|
72
|
-
if self.
|
|
82
|
+
if self._color:
|
|
73
83
|
self.log(f"{self._BOLD_BLUE}{msg}{self._RESET}")
|
|
74
84
|
else:
|
|
75
85
|
self.log(f"D: {msg}")
|
|
@@ -81,7 +91,10 @@ class SimpleColorLogger():
|
|
|
81
91
|
# Skip progress updates (overwrite without newline) in non-TTY mode
|
|
82
92
|
if overwrite and not self._is_tty and end != '\n':
|
|
83
93
|
return
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
94
|
+
if not overwrite:
|
|
95
|
+
self._clear_pending()
|
|
96
|
+
color_code = self.COLORS.get(color, self._BOLD_GREEN) if self._color else ''
|
|
97
|
+
reset_code = self._RESET if self._color else ''
|
|
98
|
+
clear = f'\r{self._CLEAR_LINE}' if self._color and overwrite else ('\r' if self._is_tty and overwrite else '')
|
|
87
99
|
print(f"{clear}{color_code}{msg}{reset_code}", end=end, file=_sys.stderr, flush=True)
|
|
100
|
+
self._pending_line = (end != '\n')
|
|
@@ -20,13 +20,42 @@ class MpyError(Exception):
|
|
|
20
20
|
|
|
21
21
|
class CmdError(MpyError):
|
|
22
22
|
"""Command execution error on device"""
|
|
23
|
+
|
|
24
|
+
# Known MicroPython OSError codes
|
|
25
|
+
_OSERROR_MESSAGES = {
|
|
26
|
+
'2': 'No such file or directory',
|
|
27
|
+
'13': 'Permission denied',
|
|
28
|
+
'17': 'File exists',
|
|
29
|
+
'19': 'No such device',
|
|
30
|
+
'21': 'Is a directory',
|
|
31
|
+
'22': 'Invalid argument',
|
|
32
|
+
'28': 'No space left on device',
|
|
33
|
+
'30': 'Read-only filesystem',
|
|
34
|
+
'110': 'Connection timed out',
|
|
35
|
+
'113': 'No route to host',
|
|
36
|
+
}
|
|
37
|
+
|
|
23
38
|
def __init__(self, cmd, result, error):
|
|
24
39
|
self._cmd = cmd
|
|
25
40
|
self._result = result
|
|
26
41
|
self._error = error.decode('utf-8')
|
|
27
42
|
super().__init__(self.__str__())
|
|
28
43
|
|
|
44
|
+
def _friendly_error(self):
|
|
45
|
+
"""Translate known OSError codes to human-readable messages"""
|
|
46
|
+
import re
|
|
47
|
+
match = re.search(r'OSError: (\d+)', self._error)
|
|
48
|
+
if match:
|
|
49
|
+
code = match.group(1)
|
|
50
|
+
msg = self._OSERROR_MESSAGES.get(code)
|
|
51
|
+
if msg:
|
|
52
|
+
return f'OSError: {msg} (errno {code})'
|
|
53
|
+
return None
|
|
54
|
+
|
|
29
55
|
def __str__(self):
|
|
56
|
+
friendly = self._friendly_error()
|
|
57
|
+
if friendly:
|
|
58
|
+
return friendly
|
|
30
59
|
res = f'Command:\n {self._cmd}\n'
|
|
31
60
|
if self._result:
|
|
32
61
|
res += f'Result:\n {self._result}\n'
|
|
@@ -30,7 +30,7 @@ def _join_remote_path(base, name):
|
|
|
30
30
|
if base == '/':
|
|
31
31
|
return '/' + name
|
|
32
32
|
elif base:
|
|
33
|
-
return base + '/' + name
|
|
33
|
+
return base.rstrip('/') + '/' + name
|
|
34
34
|
else:
|
|
35
35
|
return name
|
|
36
36
|
|
|
@@ -119,7 +119,10 @@ class MpyTool():
|
|
|
119
119
|
chunk_str = f"{chunk // 1024}K" if chunk >= 1024 else str(chunk)
|
|
120
120
|
compress = self._mpy._detect_deflate() if self._compress is None else self._compress
|
|
121
121
|
compress_str = "on" if compress else "off"
|
|
122
|
-
self.
|
|
122
|
+
if self._verbose >= 2:
|
|
123
|
+
self.verbose(f"COPY (chunk: {chunk_str}, compress: {compress_str})")
|
|
124
|
+
else:
|
|
125
|
+
self.verbose("COPY")
|
|
123
126
|
|
|
124
127
|
@staticmethod
|
|
125
128
|
def _format_local_path(path):
|
|
@@ -157,7 +160,7 @@ class MpyTool():
|
|
|
157
160
|
return info
|
|
158
161
|
|
|
159
162
|
def _format_line(self, status, total, encodings=None):
|
|
160
|
-
"""Format
|
|
163
|
+
"""Format verbose line: [2/5] 100% 24.1K source -> dest (base64)"""
|
|
161
164
|
size_str = self.format_size(total)
|
|
162
165
|
multi = self._progress_total_files > 1
|
|
163
166
|
if multi:
|
|
@@ -170,6 +173,22 @@ class MpyTool():
|
|
|
170
173
|
enc = self._format_encoding_info(encodings, pad=multi) if encodings else (" " * self._ENC_WIDTH if multi else "")
|
|
171
174
|
return f"{prefix:>7} {status} {size_str:>5} {self._progress_src:<{src_w}} -> {self._progress_dst:<{dst_w}}{enc}"
|
|
172
175
|
|
|
176
|
+
def _format_compact_progress(self, status, total):
|
|
177
|
+
"""Format compact progress line: [2/5] 45% 24.1K source"""
|
|
178
|
+
size_str = self.format_size(total)
|
|
179
|
+
multi = self._progress_total_files > 1
|
|
180
|
+
if multi:
|
|
181
|
+
width = len(str(self._progress_total_files))
|
|
182
|
+
prefix = f"[{self._progress_current_file:>{width}}/{self._progress_total_files}]"
|
|
183
|
+
else:
|
|
184
|
+
prefix = ""
|
|
185
|
+
return f"{prefix:>7} {status} {size_str:>5} {self._progress_src}"
|
|
186
|
+
|
|
187
|
+
def _format_compact_complete(self, total):
|
|
188
|
+
"""Format compact complete line: 24.1K source -> dest"""
|
|
189
|
+
size_str = self.format_size(total)
|
|
190
|
+
return f" {size_str:>5} {self._progress_src} -> {self._progress_dst}"
|
|
191
|
+
|
|
173
192
|
def _format_progress_line(self, percent, total, encodings=None):
|
|
174
193
|
return self._format_line(f"{percent:3d}%", total, encodings)
|
|
175
194
|
|
|
@@ -179,19 +198,22 @@ class MpyTool():
|
|
|
179
198
|
def _progress_callback(self, transferred, total):
|
|
180
199
|
"""Callback for file transfer progress"""
|
|
181
200
|
percent = (transferred * 100 // total) if total > 0 else 100
|
|
182
|
-
|
|
201
|
+
if self._verbose >= 2:
|
|
202
|
+
line = self._format_progress_line(percent, total)
|
|
203
|
+
else:
|
|
204
|
+
line = self._format_compact_progress(f"{percent:3d}%", total)
|
|
183
205
|
if self._is_debug:
|
|
184
|
-
# Debug mode: always newlines
|
|
185
206
|
self.verbose(line, color='cyan')
|
|
186
207
|
else:
|
|
187
|
-
# Normal mode: overwrite line
|
|
188
208
|
self.verbose(line, color='cyan', end='', overwrite=True)
|
|
189
209
|
|
|
190
210
|
def _progress_complete(self, total, encodings=None):
|
|
191
211
|
"""Mark current file as complete"""
|
|
192
|
-
|
|
212
|
+
if self._verbose >= 2:
|
|
213
|
+
line = self._format_progress_line(100, total, encodings)
|
|
214
|
+
else:
|
|
215
|
+
line = self._format_compact_complete(total)
|
|
193
216
|
if self._is_debug:
|
|
194
|
-
# Already printed with newline in callback
|
|
195
217
|
pass
|
|
196
218
|
else:
|
|
197
219
|
self.verbose(line, color='cyan', overwrite=True)
|
|
@@ -258,23 +280,24 @@ class MpyTool():
|
|
|
258
280
|
# For files, add basename if dst_path ends with /
|
|
259
281
|
basename = _os.path.basename(src_path)
|
|
260
282
|
if basename and not _os.path.basename(dst_path):
|
|
261
|
-
dst_path =
|
|
283
|
+
dst_path = _join_remote_path(dst_path, basename)
|
|
262
284
|
files[dst_path] = _os.path.getsize(src_path)
|
|
263
285
|
elif _os.path.isdir(src_path):
|
|
264
286
|
# For directories, mimic _put_dir behavior
|
|
265
287
|
if add_src_basename:
|
|
266
288
|
basename = _os.path.basename(_os.path.abspath(src_path))
|
|
267
289
|
if basename:
|
|
268
|
-
dst_path =
|
|
290
|
+
dst_path = _join_remote_path(dst_path, basename)
|
|
269
291
|
for root, dirs, filenames in _os.walk(src_path, topdown=True):
|
|
270
|
-
dirs[:] =
|
|
271
|
-
filenames =
|
|
272
|
-
rel_path = _os.path.relpath(root, src_path)
|
|
292
|
+
dirs[:] = sorted(d for d in dirs if not self._is_excluded(d))
|
|
293
|
+
filenames = sorted(f for f in filenames if not self._is_excluded(f))
|
|
294
|
+
rel_path = _os.path.relpath(root, src_path).replace(_os.sep, '/')
|
|
273
295
|
if rel_path == '.':
|
|
274
296
|
rel_path = ''
|
|
275
297
|
for file_name in filenames:
|
|
276
298
|
spath = _os.path.join(root, file_name)
|
|
277
|
-
dpath =
|
|
299
|
+
dpath = _join_remote_path(
|
|
300
|
+
_join_remote_path(dst_path, rel_path), file_name)
|
|
278
301
|
files[dpath] = _os.path.getsize(spath)
|
|
279
302
|
return files
|
|
280
303
|
|
|
@@ -466,10 +489,10 @@ class MpyTool():
|
|
|
466
489
|
parts.append(f"speedup {speedup:.1f}x")
|
|
467
490
|
summary = " ".join(parts)
|
|
468
491
|
if self._skipped_files > 0:
|
|
469
|
-
file_info = f"{self._stats_transferred_files} transferred, {self._skipped_files}
|
|
492
|
+
file_info = f"{self._stats_transferred_files} transferred, {self._skipped_files} unchanged"
|
|
470
493
|
else:
|
|
471
494
|
file_info = f"{total_files} files"
|
|
472
|
-
self.verbose(f"
|
|
495
|
+
self.verbose(f" {summary} ({file_info})", color='green')
|
|
473
496
|
|
|
474
497
|
@classmethod
|
|
475
498
|
def print_tree(cls, tree, prefix='', print_size=True, first=True, last=True):
|
|
@@ -518,18 +541,26 @@ class MpyTool():
|
|
|
518
541
|
"""Upload file data to device with stats tracking and progress display"""
|
|
519
542
|
file_size = len(data)
|
|
520
543
|
self._stats_total_bytes += file_size
|
|
544
|
+
if show_progress and self._verbose >= 1:
|
|
545
|
+
self._progress_current_file += 1
|
|
546
|
+
self._set_progress_info(src_path, dst_path, False, True)
|
|
547
|
+
# Show CHCK status during checksum verification
|
|
548
|
+
if self._verbose >= 2:
|
|
549
|
+
line = self._format_line("CHCK", file_size)
|
|
550
|
+
else:
|
|
551
|
+
line = self._format_compact_progress("CHCK", file_size)
|
|
552
|
+
self.verbose(line, color='cyan', end='', overwrite=True)
|
|
521
553
|
if not self._file_needs_update(data, dst_path):
|
|
522
554
|
self._skipped_files += 1
|
|
523
|
-
if show_progress and self._verbose >=
|
|
524
|
-
self.
|
|
525
|
-
|
|
526
|
-
|
|
555
|
+
if show_progress and self._verbose >= 2:
|
|
556
|
+
self.verbose(self._format_skip_line(file_size), color='yellow', overwrite=True)
|
|
557
|
+
elif show_progress and self._verbose >= 1:
|
|
558
|
+
# Erase CHCK line (next file's progress will overwrite)
|
|
559
|
+
self.verbose("", end='', overwrite=True)
|
|
527
560
|
return False # skipped
|
|
528
561
|
self._stats_transferred_bytes += file_size
|
|
529
562
|
self._stats_transferred_files += 1
|
|
530
563
|
if show_progress and self._verbose >= 1:
|
|
531
|
-
self._progress_current_file += 1
|
|
532
|
-
self._set_progress_info(src_path, dst_path, False, True)
|
|
533
564
|
encodings, wire = self._mpy.put(data, dst_path, self._progress_callback, self._compress)
|
|
534
565
|
self._stats_wire_bytes += wire
|
|
535
566
|
self._progress_complete(file_size, encodings)
|
|
@@ -556,25 +587,25 @@ class MpyTool():
|
|
|
556
587
|
self.verbose(f"PUT DIR: {src_path} -> {dst_path}", 2)
|
|
557
588
|
created_dirs = set()
|
|
558
589
|
for path, dirs, files in _os.walk(src_path, topdown=True):
|
|
559
|
-
dirs[:] =
|
|
560
|
-
files =
|
|
590
|
+
dirs[:] = sorted(d for d in dirs if not self._is_excluded(d))
|
|
591
|
+
files = sorted(f for f in files if not self._is_excluded(f))
|
|
561
592
|
if not files:
|
|
562
593
|
continue
|
|
563
|
-
rel_path = _os.path.relpath(path, src_path)
|
|
564
|
-
|
|
565
|
-
if
|
|
566
|
-
self.verbose(f'MKDIR: {
|
|
567
|
-
self._mpy.mkdir(
|
|
568
|
-
created_dirs.add(
|
|
594
|
+
rel_path = _os.path.relpath(path, src_path).replace(_os.sep, '/')
|
|
595
|
+
remote_dir = _join_remote_path(dst_path, '' if rel_path == '.' else rel_path)
|
|
596
|
+
if remote_dir and remote_dir not in created_dirs:
|
|
597
|
+
self.verbose(f'MKDIR: {remote_dir}', 2)
|
|
598
|
+
self._mpy.mkdir(remote_dir)
|
|
599
|
+
created_dirs.add(remote_dir)
|
|
569
600
|
for file_name in files:
|
|
570
601
|
spath = _os.path.join(path, file_name)
|
|
571
602
|
with open(spath, 'rb') as f:
|
|
572
|
-
self._upload_file(f.read(), spath,
|
|
603
|
+
self._upload_file(f.read(), spath, _join_remote_path(remote_dir, file_name), show_progress)
|
|
573
604
|
|
|
574
605
|
def _put_file(self, src_path, dst_path, show_progress=True):
|
|
575
606
|
basename = _os.path.basename(src_path)
|
|
576
607
|
if basename and not _os.path.basename(dst_path):
|
|
577
|
-
dst_path =
|
|
608
|
+
dst_path = _join_remote_path(dst_path, basename)
|
|
578
609
|
self.verbose(f"PUT FILE: {src_path} -> {dst_path}", 2)
|
|
579
610
|
with open(src_path, 'rb') as f:
|
|
580
611
|
data = f.read()
|
|
@@ -597,7 +628,7 @@ class MpyTool():
|
|
|
597
628
|
self._progress_current_file += 1
|
|
598
629
|
self._set_progress_info(src_path, dst_path, True, False)
|
|
599
630
|
data = self._mpy.get(src_path, self._progress_callback)
|
|
600
|
-
self._progress_complete(len(data))
|
|
631
|
+
self._progress_complete(len(data), None)
|
|
601
632
|
else:
|
|
602
633
|
data = self._mpy.get(src_path)
|
|
603
634
|
file_size = len(data)
|
|
@@ -1396,7 +1427,7 @@ Commands (: prefix = device path, :/ = root, : = CWD):
|
|
|
1396
1427
|
--boot enter bootloader (machine.bootloader)
|
|
1397
1428
|
--dtr-boot bootloader via DTR/RTS (ESP32)
|
|
1398
1429
|
monitor print device output (Ctrl+C to stop)
|
|
1399
|
-
repl interactive REPL
|
|
1430
|
+
repl interactive REPL
|
|
1400
1431
|
exec {code} execute Python code
|
|
1401
1432
|
info show device information
|
|
1402
1433
|
flash show flash/partitions info
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""MicroPython tool: terminal connection
|
|
2
|
+
|
|
3
|
+
Base class with common terminal functionality.
|
|
4
|
+
Platform-specific implementations in terminal_unix.py and terminal_win.py.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys as _sys
|
|
8
|
+
|
|
9
|
+
AVAILABLE = False
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TerminalBase:
|
|
13
|
+
"""Common terminal functionality for interactive REPL"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, conn, log):
|
|
16
|
+
self._log = log
|
|
17
|
+
self._conn = conn
|
|
18
|
+
self._running = None
|
|
19
|
+
|
|
20
|
+
def _setup(self):
|
|
21
|
+
"""Set terminal to raw mode (platform-specific)"""
|
|
22
|
+
raise NotImplementedError
|
|
23
|
+
|
|
24
|
+
def _restore(self):
|
|
25
|
+
"""Restore original terminal mode (platform-specific)"""
|
|
26
|
+
raise NotImplementedError
|
|
27
|
+
|
|
28
|
+
def read(self):
|
|
29
|
+
"""Read input from keyboard (platform-specific)"""
|
|
30
|
+
raise NotImplementedError
|
|
31
|
+
|
|
32
|
+
def _loop(self):
|
|
33
|
+
"""Main event loop (platform-specific)"""
|
|
34
|
+
raise NotImplementedError
|
|
35
|
+
|
|
36
|
+
def write(self, buf):
|
|
37
|
+
_sys.stdout.buffer.raw.write(buf)
|
|
38
|
+
|
|
39
|
+
def _read_event_terminal(self):
|
|
40
|
+
data = self.read()
|
|
41
|
+
self._log.info('from terminal: %s', data)
|
|
42
|
+
if 0x1d in data: # CTRL + ]
|
|
43
|
+
self._running = False
|
|
44
|
+
return
|
|
45
|
+
if data:
|
|
46
|
+
self._conn.write(data)
|
|
47
|
+
|
|
48
|
+
def _read_event_device(self):
|
|
49
|
+
data = self._conn.read()
|
|
50
|
+
self._log.info('from device: %s', data)
|
|
51
|
+
if data:
|
|
52
|
+
self.write(data)
|
|
53
|
+
|
|
54
|
+
def _flush_device(self):
|
|
55
|
+
data = self._conn.flush()
|
|
56
|
+
if data:
|
|
57
|
+
self.write(data)
|
|
58
|
+
|
|
59
|
+
def run(self):
|
|
60
|
+
self._setup()
|
|
61
|
+
self._running = True
|
|
62
|
+
try:
|
|
63
|
+
self._flush_device()
|
|
64
|
+
self._loop()
|
|
65
|
+
except OSError as err:
|
|
66
|
+
if self._log:
|
|
67
|
+
self._log.error(err)
|
|
68
|
+
self._restore()
|
|
69
|
+
self.write(b'\r\n')
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
try:
|
|
73
|
+
from mpytool.terminal_unix import Terminal
|
|
74
|
+
AVAILABLE = True
|
|
75
|
+
except ImportError:
|
|
76
|
+
try:
|
|
77
|
+
from mpytool.terminal_win import Terminal
|
|
78
|
+
AVAILABLE = True
|
|
79
|
+
except ImportError:
|
|
80
|
+
pass
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""MicroPython tool: Unix terminal (tty/termios/select)"""
|
|
2
|
+
|
|
3
|
+
import sys as _sys
|
|
4
|
+
import select as _select
|
|
5
|
+
import tty as _tty
|
|
6
|
+
import termios as _termios
|
|
7
|
+
|
|
8
|
+
from mpytool.terminal import TerminalBase
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Terminal(TerminalBase):
|
|
12
|
+
|
|
13
|
+
def __init__(self, conn, log):
|
|
14
|
+
super().__init__(conn, log)
|
|
15
|
+
self._stdin_fd = _sys.stdin.fileno()
|
|
16
|
+
self._orig_attr = _termios.tcgetattr(self._stdin_fd)
|
|
17
|
+
|
|
18
|
+
def __del__(self):
|
|
19
|
+
if self._orig_attr:
|
|
20
|
+
_termios.tcsetattr(
|
|
21
|
+
self._stdin_fd, _termios.TCSANOW, self._orig_attr)
|
|
22
|
+
|
|
23
|
+
def _setup(self):
|
|
24
|
+
_tty.setraw(self._stdin_fd)
|
|
25
|
+
|
|
26
|
+
def _restore(self):
|
|
27
|
+
if self._orig_attr:
|
|
28
|
+
_termios.tcsetattr(
|
|
29
|
+
self._stdin_fd, _termios.TCSANOW, self._orig_attr)
|
|
30
|
+
self._orig_attr = None
|
|
31
|
+
|
|
32
|
+
def read(self):
|
|
33
|
+
return _sys.stdin.buffer.raw.read(1)
|
|
34
|
+
|
|
35
|
+
def _loop(self):
|
|
36
|
+
select_fds = [self._stdin_fd, self._conn.fd]
|
|
37
|
+
self._log.info("select: %s", select_fds)
|
|
38
|
+
while self._running:
|
|
39
|
+
ret = _select.select(select_fds, [], [], 1)
|
|
40
|
+
self._log.info("selected: %s", ret)
|
|
41
|
+
if ret[0]:
|
|
42
|
+
if self._stdin_fd in ret[0]:
|
|
43
|
+
self._read_event_terminal()
|
|
44
|
+
if self._conn.fd in ret[0]:
|
|
45
|
+
self._read_event_device()
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""MicroPython tool: Windows terminal (msvcrt/ctypes)"""
|
|
2
|
+
|
|
3
|
+
import sys as _sys
|
|
4
|
+
import time as _time
|
|
5
|
+
import ctypes as _ctypes
|
|
6
|
+
import msvcrt as _msvcrt
|
|
7
|
+
|
|
8
|
+
from mpytool.terminal import TerminalBase
|
|
9
|
+
|
|
10
|
+
# kernel32 console mode constants
|
|
11
|
+
_STD_INPUT_HANDLE = -10
|
|
12
|
+
_STD_OUTPUT_HANDLE = -11
|
|
13
|
+
_ENABLE_VIRTUAL_TERMINAL_INPUT = 0x0200
|
|
14
|
+
_ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
|
|
15
|
+
|
|
16
|
+
_kernel32 = _ctypes.windll.kernel32
|
|
17
|
+
|
|
18
|
+
# Windows scan code to ANSI escape sequence mapping
|
|
19
|
+
# msvcrt.getch() returns 0x00 or 0xE0 prefix + scan code for special keys
|
|
20
|
+
_SCAN_TO_ANSI = {
|
|
21
|
+
72: b'\x1b[A', # Up
|
|
22
|
+
80: b'\x1b[B', # Down
|
|
23
|
+
77: b'\x1b[C', # Right
|
|
24
|
+
75: b'\x1b[D', # Left
|
|
25
|
+
71: b'\x1b[H', # Home
|
|
26
|
+
79: b'\x1b[F', # End
|
|
27
|
+
83: b'\x1b[3~', # Delete
|
|
28
|
+
82: b'\x1b[2~', # Insert
|
|
29
|
+
73: b'\x1b[5~', # Page Up
|
|
30
|
+
81: b'\x1b[6~', # Page Down
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class Terminal(TerminalBase):
|
|
35
|
+
|
|
36
|
+
def __init__(self, conn, log):
|
|
37
|
+
super().__init__(conn, log)
|
|
38
|
+
self._stdin_h = _kernel32.GetStdHandle(_STD_INPUT_HANDLE)
|
|
39
|
+
self._stdout_h = _kernel32.GetStdHandle(_STD_OUTPUT_HANDLE)
|
|
40
|
+
self._orig_in_mode = _ctypes.c_uint32()
|
|
41
|
+
self._orig_out_mode = _ctypes.c_uint32()
|
|
42
|
+
_kernel32.GetConsoleMode(
|
|
43
|
+
self._stdin_h, _ctypes.byref(self._orig_in_mode))
|
|
44
|
+
_kernel32.GetConsoleMode(
|
|
45
|
+
self._stdout_h, _ctypes.byref(self._orig_out_mode))
|
|
46
|
+
|
|
47
|
+
def __del__(self):
|
|
48
|
+
self._restore()
|
|
49
|
+
|
|
50
|
+
def _setup(self):
|
|
51
|
+
# Input: disable line input, echo, processed input
|
|
52
|
+
# enable virtual terminal input (Win10+)
|
|
53
|
+
_kernel32.SetConsoleMode(
|
|
54
|
+
self._stdin_h, _ENABLE_VIRTUAL_TERMINAL_INPUT)
|
|
55
|
+
# Output: enable ANSI escape processing
|
|
56
|
+
out_mode = self._orig_out_mode.value | _ENABLE_VIRTUAL_TERMINAL_PROCESSING
|
|
57
|
+
_kernel32.SetConsoleMode(self._stdout_h, out_mode)
|
|
58
|
+
|
|
59
|
+
def _restore(self):
|
|
60
|
+
if self._orig_in_mode.value:
|
|
61
|
+
_kernel32.SetConsoleMode(self._stdin_h, self._orig_in_mode)
|
|
62
|
+
self._orig_in_mode = _ctypes.c_uint32()
|
|
63
|
+
if self._orig_out_mode.value:
|
|
64
|
+
_kernel32.SetConsoleMode(self._stdout_h, self._orig_out_mode)
|
|
65
|
+
self._orig_out_mode = _ctypes.c_uint32()
|
|
66
|
+
|
|
67
|
+
def write(self, buf):
|
|
68
|
+
_sys.stdout.buffer.raw.write(buf)
|
|
69
|
+
_sys.stdout.buffer.raw.flush()
|
|
70
|
+
|
|
71
|
+
def read(self):
|
|
72
|
+
byte = _msvcrt.getch()
|
|
73
|
+
if byte in (b'\x00', b'\xe0'):
|
|
74
|
+
scan = _msvcrt.getch()
|
|
75
|
+
return _SCAN_TO_ANSI.get(scan[0], b'')
|
|
76
|
+
return byte
|
|
77
|
+
|
|
78
|
+
def _loop(self):
|
|
79
|
+
while self._running:
|
|
80
|
+
activity = False
|
|
81
|
+
if _msvcrt.kbhit():
|
|
82
|
+
self._read_event_terminal()
|
|
83
|
+
activity = True
|
|
84
|
+
if self._conn._has_data(0):
|
|
85
|
+
self._read_event_device()
|
|
86
|
+
activity = True
|
|
87
|
+
if not activity:
|
|
88
|
+
_time.sleep(0.002)
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import sys
|
|
4
4
|
|
|
5
|
+
from serial.tools.list_ports import comports as _comports
|
|
6
|
+
|
|
5
7
|
|
|
6
8
|
def is_remote_path(path: str) -> bool:
|
|
7
9
|
"""Check if path is remote (starts with :)"""
|
|
@@ -74,7 +76,9 @@ def detect_serial_ports() -> list[str]:
|
|
|
74
76
|
"/dev/ttyACM*",
|
|
75
77
|
"/dev/ttyUSB*",
|
|
76
78
|
]
|
|
77
|
-
|
|
79
|
+
elif sys.platform == "win32":
|
|
80
|
+
return sorted(
|
|
81
|
+
p.device for p in _comports() if p.vid is not None)
|
|
78
82
|
|
|
79
83
|
ports = []
|
|
80
84
|
for pattern in patterns:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mpytool
|
|
3
|
-
Version: 2.2.
|
|
3
|
+
Version: 2.2.2
|
|
4
4
|
Summary: MPY tool - manage files on devices running MicroPython
|
|
5
5
|
Author-email: Pavel Revak <pavel.revak@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -446,7 +446,15 @@ Working only with MicroPython boards, not with CircuitPython
|
|
|
446
446
|
|
|
447
447
|
- Linux
|
|
448
448
|
- MacOS
|
|
449
|
-
- Windows
|
|
449
|
+
- Windows
|
|
450
|
+
|
|
451
|
+
### Windows notes
|
|
452
|
+
|
|
453
|
+
All commands work on Windows including `repl`.
|
|
454
|
+
|
|
455
|
+
**CMD.EXE**: ANSI colors are disabled, progress indicator works. Log messages use text prefixes (`E:`, `W:`, `I:`, `D:`).
|
|
456
|
+
|
|
457
|
+
**Git Bash**: Full color support (sets `TERM` environment variable).
|
|
450
458
|
|
|
451
459
|
## Credits
|
|
452
460
|
|
|
@@ -37,6 +37,37 @@ class TestCmdError(unittest.TestCase):
|
|
|
37
37
|
def test_is_mpy_error(self):
|
|
38
38
|
self.assertTrue(issubclass(CmdError, MpyError))
|
|
39
39
|
|
|
40
|
+
def test_friendly_oserror_no_space(self):
|
|
41
|
+
err = CmdError("f.write(b'data')", b"", b"Traceback (most recent call last):\n File \"<stdin>\", line 1, in <module>\nOSError: 28")
|
|
42
|
+
msg = str(err)
|
|
43
|
+
self.assertIn("No space left on device", msg)
|
|
44
|
+
self.assertIn("errno 28", msg)
|
|
45
|
+
self.assertNotIn("Traceback", msg)
|
|
46
|
+
|
|
47
|
+
def test_friendly_oserror_enoent(self):
|
|
48
|
+
err = CmdError("open('x')", b"", b"OSError: 2")
|
|
49
|
+
msg = str(err)
|
|
50
|
+
self.assertIn("No such file or directory", msg)
|
|
51
|
+
self.assertIn("errno 2", msg)
|
|
52
|
+
|
|
53
|
+
def test_friendly_oserror_read_only(self):
|
|
54
|
+
err = CmdError("f.write(b'')", b"", b"OSError: 30")
|
|
55
|
+
msg = str(err)
|
|
56
|
+
self.assertIn("Read-only filesystem", msg)
|
|
57
|
+
|
|
58
|
+
def test_unknown_oserror_shows_full(self):
|
|
59
|
+
err = CmdError("cmd", b"", b"OSError: 999")
|
|
60
|
+
msg = str(err)
|
|
61
|
+
self.assertIn("cmd", msg)
|
|
62
|
+
self.assertIn("OSError: 999", msg)
|
|
63
|
+
|
|
64
|
+
def test_non_oserror_shows_full(self):
|
|
65
|
+
err = CmdError("cmd", b"result", b"ValueError: bad")
|
|
66
|
+
msg = str(err)
|
|
67
|
+
self.assertIn("cmd", msg)
|
|
68
|
+
self.assertIn("result", msg)
|
|
69
|
+
self.assertIn("ValueError: bad", msg)
|
|
70
|
+
|
|
40
71
|
|
|
41
72
|
class TestPathErrors(unittest.TestCase):
|
|
42
73
|
def test_path_not_found(self):
|
|
@@ -31,6 +31,7 @@ class TestDeviceConnection(unittest.TestCase):
|
|
|
31
31
|
@classmethod
|
|
32
32
|
def tearDownClass(cls):
|
|
33
33
|
cls.mpy.comm.exit_raw_repl()
|
|
34
|
+
cls.conn.close()
|
|
34
35
|
|
|
35
36
|
def test_connection_established(self):
|
|
36
37
|
"""Test that connection is established"""
|
|
@@ -64,6 +65,7 @@ class TestFileOperations(unittest.TestCase):
|
|
|
64
65
|
except Exception:
|
|
65
66
|
pass
|
|
66
67
|
cls.mpy.comm.exit_raw_repl()
|
|
68
|
+
cls.conn.close()
|
|
67
69
|
|
|
68
70
|
def test_01_mkdir(self):
|
|
69
71
|
"""Test creating directory"""
|
|
@@ -143,6 +145,7 @@ class TestExec(unittest.TestCase):
|
|
|
143
145
|
@classmethod
|
|
144
146
|
def tearDownClass(cls):
|
|
145
147
|
cls.mpy.comm.exit_raw_repl()
|
|
148
|
+
cls.conn.close()
|
|
146
149
|
|
|
147
150
|
def test_exec_simple(self):
|
|
148
151
|
"""Test simple code execution"""
|
|
@@ -188,7 +191,7 @@ class TestReplRecovery(unittest.TestCase):
|
|
|
188
191
|
# Close connection WITHOUT exiting raw REPL
|
|
189
192
|
# This simulates a crash or unexpected disconnect
|
|
190
193
|
del mpy1
|
|
191
|
-
|
|
194
|
+
conn1.close()
|
|
192
195
|
|
|
193
196
|
# Second connection - device is still in raw REPL mode
|
|
194
197
|
conn2 = ConnSerial(port=DEVICE_PORT, baudrate=115200)
|
|
@@ -201,6 +204,7 @@ class TestReplRecovery(unittest.TestCase):
|
|
|
201
204
|
self.assertEqual(result, 2)
|
|
202
205
|
finally:
|
|
203
206
|
mpy2.comm.exit_raw_repl()
|
|
207
|
+
conn2.close()
|
|
204
208
|
|
|
205
209
|
|
|
206
210
|
@requires_device
|
|
@@ -216,6 +220,7 @@ class TestDeviceInfo(unittest.TestCase):
|
|
|
216
220
|
@classmethod
|
|
217
221
|
def tearDownClass(cls):
|
|
218
222
|
cls.mpy.comm.exit_raw_repl()
|
|
223
|
+
cls.conn.close()
|
|
219
224
|
|
|
220
225
|
def test_get_platform(self):
|
|
221
226
|
"""Test getting platform info"""
|
|
@@ -271,6 +276,7 @@ class TestCwdOperations(unittest.TestCase):
|
|
|
271
276
|
except Exception:
|
|
272
277
|
pass
|
|
273
278
|
cls.mpy.comm.exit_raw_repl()
|
|
279
|
+
cls.conn.close()
|
|
274
280
|
|
|
275
281
|
def setUp(self):
|
|
276
282
|
# Always start from root
|
|
@@ -420,6 +426,7 @@ class TestCpCommand(unittest.TestCase):
|
|
|
420
426
|
if os.path.exists(cls.LOCAL_DIR):
|
|
421
427
|
shutil.rmtree(cls.LOCAL_DIR)
|
|
422
428
|
cls.mpy.comm.exit_raw_repl()
|
|
429
|
+
cls.conn.close()
|
|
423
430
|
|
|
424
431
|
def test_01_upload_file(self):
|
|
425
432
|
"""Test cp local file to remote"""
|
|
@@ -524,6 +531,7 @@ class TestMvCommand(unittest.TestCase):
|
|
|
524
531
|
except Exception:
|
|
525
532
|
pass
|
|
526
533
|
cls.mpy.comm.exit_raw_repl()
|
|
534
|
+
cls.conn.close()
|
|
527
535
|
|
|
528
536
|
def test_01_rename_file(self):
|
|
529
537
|
"""Test mv rename file"""
|
|
@@ -578,6 +586,7 @@ class TestDeleteCommand(unittest.TestCase):
|
|
|
578
586
|
except Exception:
|
|
579
587
|
pass
|
|
580
588
|
cls.mpy.comm.exit_raw_repl()
|
|
589
|
+
cls.conn.close()
|
|
581
590
|
|
|
582
591
|
def test_01_delete_file(self):
|
|
583
592
|
"""Test rm single file"""
|
|
@@ -649,6 +658,7 @@ class TestSkipUnchangedFiles(unittest.TestCase):
|
|
|
649
658
|
except Exception:
|
|
650
659
|
pass
|
|
651
660
|
cls.mpy.comm.exit_raw_repl()
|
|
661
|
+
cls.conn.close()
|
|
652
662
|
|
|
653
663
|
def test_01_hashfile(self):
|
|
654
664
|
"""Test hashfile method returns correct SHA256"""
|
|
@@ -712,6 +722,7 @@ class TestEncodingAndCompression(unittest.TestCase):
|
|
|
712
722
|
except Exception:
|
|
713
723
|
pass
|
|
714
724
|
cls.mpy.comm.exit_raw_repl()
|
|
725
|
+
cls.conn.close()
|
|
715
726
|
|
|
716
727
|
def test_01_text_upload_uses_raw(self):
|
|
717
728
|
"""Test that text files use raw encoding"""
|
|
@@ -792,6 +803,7 @@ class TestCpWithFlags(unittest.TestCase):
|
|
|
792
803
|
except Exception:
|
|
793
804
|
pass
|
|
794
805
|
cls.mpy.comm.exit_raw_repl()
|
|
806
|
+
cls.conn.close()
|
|
795
807
|
|
|
796
808
|
def test_01_cp_with_combined_flags(self):
|
|
797
809
|
"""Test cp -fz with combined flags"""
|
|
@@ -845,6 +857,7 @@ class TestSleepCommand(unittest.TestCase):
|
|
|
845
857
|
@classmethod
|
|
846
858
|
def tearDownClass(cls):
|
|
847
859
|
cls.mpy.comm.exit_raw_repl()
|
|
860
|
+
cls.conn.close()
|
|
848
861
|
|
|
849
862
|
def test_sleep_delays_execution(self):
|
|
850
863
|
"""Test that sleep actually delays execution"""
|
|
@@ -886,6 +899,7 @@ class TestSpecialCharacterFilenames(unittest.TestCase):
|
|
|
886
899
|
except Exception:
|
|
887
900
|
pass
|
|
888
901
|
cls.mpy.comm.exit_raw_repl()
|
|
902
|
+
cls.conn.close()
|
|
889
903
|
|
|
890
904
|
def test_01_filename_with_equals_sign(self):
|
|
891
905
|
"""Test file with equals sign in name (mpremote #18658)"""
|
|
@@ -979,6 +993,7 @@ class TestUnicodeFilenames(unittest.TestCase):
|
|
|
979
993
|
except Exception:
|
|
980
994
|
pass
|
|
981
995
|
cls.mpy.comm.exit_raw_repl()
|
|
996
|
+
cls.conn.close()
|
|
982
997
|
|
|
983
998
|
def test_01_czech_filename(self):
|
|
984
999
|
"""Test file with Czech characters in name"""
|
|
@@ -1081,6 +1096,7 @@ class TestCpSpecialFilenames(unittest.TestCase):
|
|
|
1081
1096
|
if os.path.exists(cls.LOCAL_DIR):
|
|
1082
1097
|
shutil.rmtree(cls.LOCAL_DIR)
|
|
1083
1098
|
cls.mpy.comm.exit_raw_repl()
|
|
1099
|
+
cls.conn.close()
|
|
1084
1100
|
|
|
1085
1101
|
def test_01_upload_file_with_equals(self):
|
|
1086
1102
|
"""Test cp upload file with equals sign (mpremote #18658)"""
|
|
@@ -1182,6 +1198,7 @@ class TestErrorMessages(unittest.TestCase):
|
|
|
1182
1198
|
except Exception:
|
|
1183
1199
|
pass
|
|
1184
1200
|
cls.mpy.comm.exit_raw_repl()
|
|
1201
|
+
cls.conn.close()
|
|
1185
1202
|
|
|
1186
1203
|
def test_01_get_nonexistent_file(self):
|
|
1187
1204
|
"""Test get on nonexistent file raises FileNotFound"""
|
|
@@ -1236,6 +1253,7 @@ class TestLargeFileTransfer(unittest.TestCase):
|
|
|
1236
1253
|
except Exception:
|
|
1237
1254
|
pass
|
|
1238
1255
|
cls.mpy.comm.exit_raw_repl()
|
|
1256
|
+
cls.conn.close()
|
|
1239
1257
|
|
|
1240
1258
|
def test_01_upload_10kb_file(self):
|
|
1241
1259
|
"""Test upload of 10KB file"""
|
|
@@ -1285,6 +1303,7 @@ class TestPartitions(unittest.TestCase):
|
|
|
1285
1303
|
@classmethod
|
|
1286
1304
|
def tearDownClass(cls):
|
|
1287
1305
|
cls.mpy.comm.exit_raw_repl()
|
|
1306
|
+
cls.conn.close()
|
|
1288
1307
|
|
|
1289
1308
|
def test_01_partitions_list(self):
|
|
1290
1309
|
"""Test listing partitions on ESP32"""
|
|
@@ -1338,6 +1357,7 @@ class TestFlashRP2(unittest.TestCase):
|
|
|
1338
1357
|
@classmethod
|
|
1339
1358
|
def tearDownClass(cls):
|
|
1340
1359
|
cls.mpy.comm.exit_raw_repl()
|
|
1360
|
+
cls.conn.close()
|
|
1341
1361
|
|
|
1342
1362
|
def test_01_flash_info(self):
|
|
1343
1363
|
"""Test getting flash info on RP2"""
|
|
@@ -1392,6 +1412,7 @@ class TestRawPasteMode(unittest.TestCase):
|
|
|
1392
1412
|
@classmethod
|
|
1393
1413
|
def tearDownClass(cls):
|
|
1394
1414
|
cls.comm.exit_raw_repl()
|
|
1415
|
+
cls.conn.close()
|
|
1395
1416
|
|
|
1396
1417
|
def test_01_raw_paste_simple(self):
|
|
1397
1418
|
"""Test simple code execution via raw-paste"""
|
|
@@ -149,11 +149,13 @@ class TestDetectSerialPorts(unittest.TestCase):
|
|
|
149
149
|
self.assertIn("/dev/ttyUSB0", ports)
|
|
150
150
|
|
|
151
151
|
@patch("sys.platform", "win32")
|
|
152
|
-
@patch("
|
|
153
|
-
def
|
|
152
|
+
@patch("mpytool.utils._comports")
|
|
153
|
+
def test_windows_comports(self, mock_comports):
|
|
154
|
+
mock_port1 = type('PortInfo', (), {'device': 'COM3', 'vid': 0x2E8A})()
|
|
155
|
+
mock_port2 = type('PortInfo', (), {'device': 'COM1', 'vid': None})()
|
|
156
|
+
mock_comports.return_value = [mock_port1, mock_port2]
|
|
154
157
|
ports = detect_serial_ports()
|
|
155
|
-
self.assertEqual(ports, [])
|
|
156
|
-
mock_glob.assert_not_called()
|
|
158
|
+
self.assertEqual(ports, ['COM3'])
|
|
157
159
|
|
|
158
160
|
@patch("sys.platform", "linux")
|
|
159
161
|
@patch("glob.glob")
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
"""MicroPython tool: terminal connection"""
|
|
2
|
-
|
|
3
|
-
AVAILABLE = False
|
|
4
|
-
|
|
5
|
-
try:
|
|
6
|
-
import sys as _sys
|
|
7
|
-
import select as _select
|
|
8
|
-
import tty as _tty
|
|
9
|
-
import termios as _termios
|
|
10
|
-
|
|
11
|
-
AVAILABLE = True
|
|
12
|
-
|
|
13
|
-
class Terminal:
|
|
14
|
-
def __init__(self, conn, log):
|
|
15
|
-
self._log = log
|
|
16
|
-
self._conn = conn
|
|
17
|
-
self._stdin_fd = _sys.stdin.fileno()
|
|
18
|
-
self._last_attr = _termios.tcgetattr(self._stdin_fd)
|
|
19
|
-
self._running = None
|
|
20
|
-
|
|
21
|
-
def __del__(self):
|
|
22
|
-
if self._last_attr:
|
|
23
|
-
_termios.tcsetattr(self._stdin_fd, _termios.TCSANOW, self._last_attr)
|
|
24
|
-
|
|
25
|
-
def read(self):
|
|
26
|
-
return _sys.stdin.buffer.raw.read(1)
|
|
27
|
-
|
|
28
|
-
def write(self, buf):
|
|
29
|
-
_sys.stdout.buffer.raw.write(buf)
|
|
30
|
-
|
|
31
|
-
def _read_event_terminal(self):
|
|
32
|
-
data = self.read()
|
|
33
|
-
self._log.info('from terminal: %s', data)
|
|
34
|
-
if 0x1d in data: # CTRL + ]
|
|
35
|
-
self._running = False
|
|
36
|
-
self._conn.write(data)
|
|
37
|
-
|
|
38
|
-
def _read_event_device(self):
|
|
39
|
-
data = self._conn.read()
|
|
40
|
-
self._log.info('from device: %s', data)
|
|
41
|
-
if data:
|
|
42
|
-
self.write(data)
|
|
43
|
-
|
|
44
|
-
def _flush_device(self):
|
|
45
|
-
data = self._conn.flush()
|
|
46
|
-
if data:
|
|
47
|
-
self.write(data)
|
|
48
|
-
|
|
49
|
-
def _read_event(self, event):
|
|
50
|
-
if self._stdin_fd in event:
|
|
51
|
-
self._read_event_terminal()
|
|
52
|
-
if self._conn.fd in event:
|
|
53
|
-
self._read_event_device()
|
|
54
|
-
|
|
55
|
-
def run(self):
|
|
56
|
-
_tty.setraw(self._stdin_fd)
|
|
57
|
-
self._running = True
|
|
58
|
-
try:
|
|
59
|
-
self._flush_device()
|
|
60
|
-
select_fds = [self._stdin_fd, self._conn.fd, ]
|
|
61
|
-
self._log.info("select: %s", select_fds)
|
|
62
|
-
while self._running:
|
|
63
|
-
ret = _select.select(select_fds, [], [], 1)
|
|
64
|
-
self._log.info("selected: %s", ret)
|
|
65
|
-
if ret[0]:
|
|
66
|
-
self._read_event(ret[0])
|
|
67
|
-
except OSError as err:
|
|
68
|
-
if self._log:
|
|
69
|
-
self._log.error(err)
|
|
70
|
-
_termios.tcsetattr(self._stdin_fd, _termios.TCSANOW, self._last_attr)
|
|
71
|
-
self._last_attr = None
|
|
72
|
-
self.write(b'\r\n')
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
except ImportError:
|
|
76
|
-
pass
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|