mpytool 2.2.0__tar.gz → 2.2.1__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.1}/PKG-INFO +10 -2
- {mpytool-2.2.0 → mpytool-2.2.1}/README.md +9 -1
- {mpytool-2.2.0 → mpytool-2.2.1}/mpytool/conn.py +3 -0
- {mpytool-2.2.0 → mpytool-2.2.1}/mpytool/conn_serial.py +23 -1
- {mpytool-2.2.0 → mpytool-2.2.1}/mpytool/conn_socket.py +5 -1
- {mpytool-2.2.0 → mpytool-2.2.1}/mpytool/logger.py +11 -9
- {mpytool-2.2.0 → mpytool-2.2.1}/mpytool/mpytool.py +15 -14
- mpytool-2.2.1/mpytool/terminal.py +80 -0
- mpytool-2.2.1/mpytool/terminal_unix.py +45 -0
- mpytool-2.2.1/mpytool/terminal_win.py +88 -0
- {mpytool-2.2.0 → mpytool-2.2.1}/mpytool/utils.py +5 -1
- {mpytool-2.2.0 → mpytool-2.2.1}/mpytool.egg-info/PKG-INFO +10 -2
- {mpytool-2.2.0 → mpytool-2.2.1}/mpytool.egg-info/SOURCES.txt +2 -0
- {mpytool-2.2.0 → mpytool-2.2.1}/pyproject.toml +1 -1
- {mpytool-2.2.0 → mpytool-2.2.1}/tests/test_integration.py +22 -1
- {mpytool-2.2.0 → mpytool-2.2.1}/tests/test_utils.py +6 -4
- mpytool-2.2.0/mpytool/terminal.py +0 -76
- {mpytool-2.2.0 → mpytool-2.2.1}/LICENSE +0 -0
- {mpytool-2.2.0 → mpytool-2.2.1}/mpytool/__init__.py +0 -0
- {mpytool-2.2.0 → mpytool-2.2.1}/mpytool/mpy.py +0 -0
- {mpytool-2.2.0 → mpytool-2.2.1}/mpytool/mpy_comm.py +0 -0
- {mpytool-2.2.0 → mpytool-2.2.1}/mpytool.egg-info/dependency_links.txt +0 -0
- {mpytool-2.2.0 → mpytool-2.2.1}/mpytool.egg-info/entry_points.txt +0 -0
- {mpytool-2.2.0 → mpytool-2.2.1}/mpytool.egg-info/requires.txt +0 -0
- {mpytool-2.2.0 → mpytool-2.2.1}/mpytool.egg-info/top_level.txt +0 -0
- {mpytool-2.2.0 → mpytool-2.2.1}/setup.cfg +0 -0
- {mpytool-2.2.0 → mpytool-2.2.1}/tests/test_errors.py +0 -0
- {mpytool-2.2.0 → mpytool-2.2.1}/tests/test_mpy.py +0 -0
- {mpytool-2.2.0 → mpytool-2.2.1}/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.1
|
|
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,11 +28,13 @@ 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
|
)
|
|
37
39
|
|
|
38
40
|
def log(self, msg):
|
|
@@ -42,7 +44,7 @@ class SimpleColorLogger():
|
|
|
42
44
|
if args:
|
|
43
45
|
msg = msg % args
|
|
44
46
|
if self._loglevel >= 1:
|
|
45
|
-
if self.
|
|
47
|
+
if self._color:
|
|
46
48
|
self.log(f"{self._BOLD_RED}{msg}{self._RESET}")
|
|
47
49
|
else:
|
|
48
50
|
self.log(f"E: {msg}")
|
|
@@ -51,7 +53,7 @@ class SimpleColorLogger():
|
|
|
51
53
|
if args:
|
|
52
54
|
msg = msg % args
|
|
53
55
|
if self._loglevel >= 2:
|
|
54
|
-
if self.
|
|
56
|
+
if self._color:
|
|
55
57
|
self.log(f"{self._BOLD_YELLOW}{msg}{self._RESET}")
|
|
56
58
|
else:
|
|
57
59
|
self.log(f"W: {msg}")
|
|
@@ -60,7 +62,7 @@ class SimpleColorLogger():
|
|
|
60
62
|
if args:
|
|
61
63
|
msg = msg % args
|
|
62
64
|
if self._loglevel >= 3:
|
|
63
|
-
if self.
|
|
65
|
+
if self._color:
|
|
64
66
|
self.log(f"{self._BOLD_MAGENTA}{msg}{self._RESET}")
|
|
65
67
|
else:
|
|
66
68
|
self.log(f"I: {msg}")
|
|
@@ -69,7 +71,7 @@ class SimpleColorLogger():
|
|
|
69
71
|
if args:
|
|
70
72
|
msg = msg % args
|
|
71
73
|
if self._loglevel >= 4:
|
|
72
|
-
if self.
|
|
74
|
+
if self._color:
|
|
73
75
|
self.log(f"{self._BOLD_BLUE}{msg}{self._RESET}")
|
|
74
76
|
else:
|
|
75
77
|
self.log(f"D: {msg}")
|
|
@@ -81,7 +83,7 @@ class SimpleColorLogger():
|
|
|
81
83
|
# Skip progress updates (overwrite without newline) in non-TTY mode
|
|
82
84
|
if overwrite and not self._is_tty and end != '\n':
|
|
83
85
|
return
|
|
84
|
-
color_code = self.COLORS.get(color, self._BOLD_GREEN) if self.
|
|
85
|
-
reset_code = self._RESET if self.
|
|
86
|
-
clear = f'\r{self._CLEAR_LINE}' if self._is_tty and overwrite else ''
|
|
86
|
+
color_code = self.COLORS.get(color, self._BOLD_GREEN) if self._color else ''
|
|
87
|
+
reset_code = self._RESET if self._color else ''
|
|
88
|
+
clear = f'\r{self._CLEAR_LINE}' if self._color and overwrite else ('\r' if self._is_tty and overwrite else '')
|
|
87
89
|
print(f"{clear}{color_code}{msg}{reset_code}", end=end, file=_sys.stderr, flush=True)
|
|
@@ -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
|
|
|
@@ -258,23 +258,24 @@ class MpyTool():
|
|
|
258
258
|
# For files, add basename if dst_path ends with /
|
|
259
259
|
basename = _os.path.basename(src_path)
|
|
260
260
|
if basename and not _os.path.basename(dst_path):
|
|
261
|
-
dst_path =
|
|
261
|
+
dst_path = _join_remote_path(dst_path, basename)
|
|
262
262
|
files[dst_path] = _os.path.getsize(src_path)
|
|
263
263
|
elif _os.path.isdir(src_path):
|
|
264
264
|
# For directories, mimic _put_dir behavior
|
|
265
265
|
if add_src_basename:
|
|
266
266
|
basename = _os.path.basename(_os.path.abspath(src_path))
|
|
267
267
|
if basename:
|
|
268
|
-
dst_path =
|
|
268
|
+
dst_path = _join_remote_path(dst_path, basename)
|
|
269
269
|
for root, dirs, filenames in _os.walk(src_path, topdown=True):
|
|
270
270
|
dirs[:] = [d for d in dirs if not self._is_excluded(d)]
|
|
271
271
|
filenames = [f for f in filenames if not self._is_excluded(f)]
|
|
272
|
-
rel_path = _os.path.relpath(root, src_path)
|
|
272
|
+
rel_path = _os.path.relpath(root, src_path).replace(_os.sep, '/')
|
|
273
273
|
if rel_path == '.':
|
|
274
274
|
rel_path = ''
|
|
275
275
|
for file_name in filenames:
|
|
276
276
|
spath = _os.path.join(root, file_name)
|
|
277
|
-
dpath =
|
|
277
|
+
dpath = _join_remote_path(
|
|
278
|
+
_join_remote_path(dst_path, rel_path), file_name)
|
|
278
279
|
files[dpath] = _os.path.getsize(spath)
|
|
279
280
|
return files
|
|
280
281
|
|
|
@@ -560,21 +561,21 @@ class MpyTool():
|
|
|
560
561
|
files = [f for f in files if not self._is_excluded(f)]
|
|
561
562
|
if not files:
|
|
562
563
|
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(
|
|
564
|
+
rel_path = _os.path.relpath(path, src_path).replace(_os.sep, '/')
|
|
565
|
+
remote_dir = _join_remote_path(dst_path, '' if rel_path == '.' else rel_path)
|
|
566
|
+
if remote_dir and remote_dir not in created_dirs:
|
|
567
|
+
self.verbose(f'MKDIR: {remote_dir}', 2)
|
|
568
|
+
self._mpy.mkdir(remote_dir)
|
|
569
|
+
created_dirs.add(remote_dir)
|
|
569
570
|
for file_name in files:
|
|
570
571
|
spath = _os.path.join(path, file_name)
|
|
571
572
|
with open(spath, 'rb') as f:
|
|
572
|
-
self._upload_file(f.read(), spath,
|
|
573
|
+
self._upload_file(f.read(), spath, _join_remote_path(remote_dir, file_name), show_progress)
|
|
573
574
|
|
|
574
575
|
def _put_file(self, src_path, dst_path, show_progress=True):
|
|
575
576
|
basename = _os.path.basename(src_path)
|
|
576
577
|
if basename and not _os.path.basename(dst_path):
|
|
577
|
-
dst_path =
|
|
578
|
+
dst_path = _join_remote_path(dst_path, basename)
|
|
578
579
|
self.verbose(f"PUT FILE: {src_path} -> {dst_path}", 2)
|
|
579
580
|
with open(src_path, 'rb') as f:
|
|
580
581
|
data = f.read()
|
|
@@ -1396,7 +1397,7 @@ Commands (: prefix = device path, :/ = root, : = CWD):
|
|
|
1396
1397
|
--boot enter bootloader (machine.bootloader)
|
|
1397
1398
|
--dtr-boot bootloader via DTR/RTS (ESP32)
|
|
1398
1399
|
monitor print device output (Ctrl+C to stop)
|
|
1399
|
-
repl interactive REPL
|
|
1400
|
+
repl interactive REPL
|
|
1400
1401
|
exec {code} execute Python code
|
|
1401
1402
|
info show device information
|
|
1402
1403
|
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.1
|
|
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
|
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|