mpytool 2.1.0__py3-none-any.whl → 2.2.0__py3-none-any.whl
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/conn_serial.py +16 -6
- mpytool/mpy.py +26 -3
- mpytool/mpytool.py +407 -345
- mpytool/utils.py +2 -3
- {mpytool-2.1.0.dist-info → mpytool-2.2.0.dist-info}/METADATA +41 -30
- mpytool-2.2.0.dist-info/RECORD +16 -0
- mpytool-2.1.0.dist-info/RECORD +0 -16
- {mpytool-2.1.0.dist-info → mpytool-2.2.0.dist-info}/WHEEL +0 -0
- {mpytool-2.1.0.dist-info → mpytool-2.2.0.dist-info}/entry_points.txt +0 -0
- {mpytool-2.1.0.dist-info → mpytool-2.2.0.dist-info}/licenses/LICENSE +0 -0
- {mpytool-2.1.0.dist-info → mpytool-2.2.0.dist-info}/top_level.txt +0 -0
mpytool/conn_serial.py
CHANGED
|
@@ -6,7 +6,7 @@ import mpytool.conn as _conn
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class ConnSerial(_conn.Conn):
|
|
9
|
-
RECONNECT_TIMEOUT =
|
|
9
|
+
RECONNECT_TIMEOUT = 10 # seconds
|
|
10
10
|
|
|
11
11
|
def __init__(self, log=None, **serial_config):
|
|
12
12
|
super().__init__(log)
|
|
@@ -28,14 +28,24 @@ class ConnSerial(_conn.Conn):
|
|
|
28
28
|
|
|
29
29
|
def _read_available(self):
|
|
30
30
|
"""Read available data from serial port"""
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
if self._serial is None:
|
|
32
|
+
raise _conn.ConnError("Not connected")
|
|
33
|
+
try:
|
|
34
|
+
in_waiting = self._serial.in_waiting
|
|
35
|
+
if in_waiting > 0:
|
|
36
|
+
return self._serial.read(in_waiting)
|
|
37
|
+
return None
|
|
38
|
+
except OSError as err:
|
|
39
|
+
raise _conn.ConnError(f"Connection lost: {err}") from err
|
|
35
40
|
|
|
36
41
|
def _write_raw(self, data):
|
|
37
42
|
"""Write data to serial port"""
|
|
38
|
-
|
|
43
|
+
if self._serial is None:
|
|
44
|
+
raise _conn.ConnError("Not connected")
|
|
45
|
+
try:
|
|
46
|
+
return self._serial.write(data)
|
|
47
|
+
except OSError as err:
|
|
48
|
+
raise _conn.ConnError(f"Connection lost: {err}") from err
|
|
39
49
|
|
|
40
50
|
def _is_usb_cdc(self):
|
|
41
51
|
"""Check if this is a USB-CDC port (native USB on ESP32-S/C)"""
|
mpytool/mpy.py
CHANGED
|
@@ -65,7 +65,8 @@ def _mt_tree(p):
|
|
|
65
65
|
def _mt_mkdir(p):
|
|
66
66
|
p=p.rstrip('/');c='';f=1
|
|
67
67
|
for d in p.split('/'):
|
|
68
|
-
c
|
|
68
|
+
if not d:c='/';continue
|
|
69
|
+
c='/'+d if c=='/' else (c+'/'+d if c else d)
|
|
69
70
|
if f:
|
|
70
71
|
try:
|
|
71
72
|
if os.stat(c)[0]=={_ATTR_FILE}:return 1
|
|
@@ -295,6 +296,27 @@ def _mt_pfind(label):
|
|
|
295
296
|
self.import_module('os')
|
|
296
297
|
self._mpy_comm.exec(f"os.rename('{_escape_path(src)}', '{_escape_path(dst)}')")
|
|
297
298
|
|
|
299
|
+
def getcwd(self):
|
|
300
|
+
"""Get current working directory
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
current working directory path
|
|
304
|
+
"""
|
|
305
|
+
self.import_module('os')
|
|
306
|
+
return self._mpy_comm.exec_eval("repr(os.getcwd())")
|
|
307
|
+
|
|
308
|
+
def chdir(self, path):
|
|
309
|
+
"""Change current working directory
|
|
310
|
+
|
|
311
|
+
Arguments:
|
|
312
|
+
path: directory path to change to
|
|
313
|
+
"""
|
|
314
|
+
self.import_module('os')
|
|
315
|
+
try:
|
|
316
|
+
self._mpy_comm.exec(f"os.chdir('{_escape_path(path)}')")
|
|
317
|
+
except _mpy_comm.CmdError as err:
|
|
318
|
+
raise DirNotFound(path) from err
|
|
319
|
+
|
|
298
320
|
def hashfile(self, path):
|
|
299
321
|
"""Compute SHA256 hash of file
|
|
300
322
|
|
|
@@ -1124,11 +1146,12 @@ def _mt_pfind(label):
|
|
|
1124
1146
|
self._mpy_comm.soft_reset_raw()
|
|
1125
1147
|
self.reset_state()
|
|
1126
1148
|
|
|
1127
|
-
def machine_reset(self, reconnect=True):
|
|
1149
|
+
def machine_reset(self, reconnect=True, timeout=None):
|
|
1128
1150
|
"""MCU reset using machine.reset()
|
|
1129
1151
|
|
|
1130
1152
|
Arguments:
|
|
1131
1153
|
reconnect: if True, attempt to reconnect after reset
|
|
1154
|
+
timeout: reconnect timeout in seconds (None = default)
|
|
1132
1155
|
|
|
1133
1156
|
Returns:
|
|
1134
1157
|
True if reconnected successfully, False otherwise
|
|
@@ -1139,7 +1162,7 @@ def _mt_pfind(label):
|
|
|
1139
1162
|
self._conn.write(b"import machine; machine.reset()\x04")
|
|
1140
1163
|
self.reset_state()
|
|
1141
1164
|
if reconnect:
|
|
1142
|
-
self._conn.reconnect()
|
|
1165
|
+
self._conn.reconnect(timeout=timeout)
|
|
1143
1166
|
return True
|
|
1144
1167
|
return False
|
|
1145
1168
|
|
mpytool/mpytool.py
CHANGED
|
@@ -23,6 +23,38 @@ class ParamsError(_mpytool.MpyError):
|
|
|
23
23
|
"""Invalid command parameters"""
|
|
24
24
|
|
|
25
25
|
|
|
26
|
+
def _join_remote_path(base, name):
|
|
27
|
+
"""Join remote path components (handles empty string and '/' correctly)"""
|
|
28
|
+
if not name:
|
|
29
|
+
return base
|
|
30
|
+
if base == '/':
|
|
31
|
+
return '/' + name
|
|
32
|
+
elif base:
|
|
33
|
+
return base + '/' + name
|
|
34
|
+
else:
|
|
35
|
+
return name
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _remote_basename(path):
|
|
39
|
+
"""Get basename from remote path"""
|
|
40
|
+
return path.rstrip('/').split('/')[-1]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _parse_device_path(path, cmd_name):
|
|
44
|
+
"""Parse device path with : prefix, raise ParamsError if missing
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
path: path string (should start with :)
|
|
48
|
+
cmd_name: command name for error message
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
path without : prefix
|
|
52
|
+
"""
|
|
53
|
+
if not path.startswith(':'):
|
|
54
|
+
raise ParamsError(f'{cmd_name} requires device path (: prefix): {path}')
|
|
55
|
+
return path[1:]
|
|
56
|
+
|
|
57
|
+
|
|
26
58
|
class MpyTool():
|
|
27
59
|
SPACE = ' '
|
|
28
60
|
BRANCH = '│ '
|
|
@@ -128,7 +160,11 @@ class MpyTool():
|
|
|
128
160
|
"""Format progress/skip line: [2/5] 100% 24.1K source -> dest (base64)"""
|
|
129
161
|
size_str = self.format_size(total)
|
|
130
162
|
multi = self._progress_total_files > 1
|
|
131
|
-
|
|
163
|
+
if multi:
|
|
164
|
+
width = len(str(self._progress_total_files))
|
|
165
|
+
prefix = f"[{self._progress_current_file:>{width}}/{self._progress_total_files}]"
|
|
166
|
+
else:
|
|
167
|
+
prefix = ""
|
|
132
168
|
src_w = max(len(self._progress_src), self._progress_max_src_len)
|
|
133
169
|
dst_w = max(len(self._progress_dst), self._progress_max_dst_len)
|
|
134
170
|
enc = self._format_encoding_info(encodings, pad=multi) if encodings else (" " * self._ENC_WIDTH if multi else "")
|
|
@@ -167,7 +203,7 @@ class MpyTool():
|
|
|
167
203
|
else:
|
|
168
204
|
self._progress_src = self._format_local_path(src)
|
|
169
205
|
if is_dst_remote:
|
|
170
|
-
self._progress_dst = ':' + dst
|
|
206
|
+
self._progress_dst = ':' + ('/' + dst if not dst.startswith('/') else dst)
|
|
171
207
|
else:
|
|
172
208
|
self._progress_dst = self._format_local_path(dst)
|
|
173
209
|
if len(self._progress_dst) > self._progress_max_dst_len:
|
|
@@ -294,9 +330,6 @@ class MpyTool():
|
|
|
294
330
|
paths.extend(self._collect_remote_paths(src_path))
|
|
295
331
|
else:
|
|
296
332
|
paths.extend(self._collect_local_paths(src_path))
|
|
297
|
-
elif cmd == 'put' and len(commands) >= 2:
|
|
298
|
-
src_path = commands[1]
|
|
299
|
-
paths.extend(self._collect_local_paths(src_path))
|
|
300
333
|
return paths
|
|
301
334
|
|
|
302
335
|
def _collect_remote_paths(self, path):
|
|
@@ -348,15 +381,9 @@ class MpyTool():
|
|
|
348
381
|
# remote -> remote (file only)
|
|
349
382
|
stat = self._mpy.stat(src_path)
|
|
350
383
|
if stat is not None and stat >= 0:
|
|
351
|
-
|
|
352
|
-
dst_paths.append(':' +
|
|
384
|
+
dst = _join_remote_path(dest_path, _remote_basename(src_path)) if dest_is_dir else dest_path
|
|
385
|
+
dst_paths.append(':' + dst)
|
|
353
386
|
return dst_paths
|
|
354
|
-
elif cmd == 'put' and len(commands) >= 2:
|
|
355
|
-
src_path = commands[1]
|
|
356
|
-
dst_path = commands[2] if len(commands) > 2 else '/'
|
|
357
|
-
if _os.path.exists(src_path):
|
|
358
|
-
files = self._collect_dst_files(src_path, dst_path.rstrip('/') or '/', add_src_basename=True)
|
|
359
|
-
return [':' + p for p in files]
|
|
360
387
|
return []
|
|
361
388
|
|
|
362
389
|
def _collect_remote_to_local_dst(self, src_path, dest_path, dest_is_dir, copy_contents):
|
|
@@ -366,10 +393,10 @@ class MpyTool():
|
|
|
366
393
|
return []
|
|
367
394
|
base_dst = dest_path.rstrip('/') or '.'
|
|
368
395
|
if dest_is_dir and not copy_contents and src_path != '/':
|
|
369
|
-
base_dst = _os.path.join(base_dst, src_path
|
|
396
|
+
base_dst = _os.path.join(base_dst, _remote_basename(src_path))
|
|
370
397
|
if stat >= 0: # file
|
|
371
398
|
if _os.path.isdir(base_dst) or dest_is_dir:
|
|
372
|
-
return [self._format_local_path(_os.path.join(base_dst, src_path
|
|
399
|
+
return [self._format_local_path(_os.path.join(base_dst, _remote_basename(src_path)))]
|
|
373
400
|
return [self._format_local_path(base_dst)]
|
|
374
401
|
return self._collect_remote_dir_dst(src_path, base_dst)
|
|
375
402
|
|
|
@@ -444,14 +471,6 @@ class MpyTool():
|
|
|
444
471
|
file_info = f"{total_files} files"
|
|
445
472
|
self.verbose(f" {summary} ({file_info})", color='green')
|
|
446
473
|
|
|
447
|
-
def cmd_ls(self, dir_name):
|
|
448
|
-
result = self._mpy.ls(dir_name)
|
|
449
|
-
for name, size in result:
|
|
450
|
-
if size is not None:
|
|
451
|
-
print(f'{self.format_size(size):>9} {name}')
|
|
452
|
-
else:
|
|
453
|
-
print(f'{"":9} {name}/')
|
|
454
|
-
|
|
455
474
|
@classmethod
|
|
456
475
|
def print_tree(cls, tree, prefix='', print_size=True, first=True, last=True):
|
|
457
476
|
"""Print tree of files
|
|
@@ -464,12 +483,14 @@ class MpyTool():
|
|
|
464
483
|
else:
|
|
465
484
|
this_prefix = cls.TEE
|
|
466
485
|
sufix = ''
|
|
467
|
-
if sub_tree is not None and name !=
|
|
486
|
+
if sub_tree is not None and name != '/':
|
|
468
487
|
sufix = '/'
|
|
488
|
+
# For root, display './' only for empty path (CWD)
|
|
489
|
+
display_name = '.' if first and name in ('', '.') else name
|
|
469
490
|
line = ''
|
|
470
491
|
if print_size:
|
|
471
492
|
line += f'{cls.format_size(size):>9} '
|
|
472
|
-
line += prefix + this_prefix +
|
|
493
|
+
line += prefix + this_prefix + display_name + sufix
|
|
473
494
|
print(line)
|
|
474
495
|
if not sub_tree:
|
|
475
496
|
return
|
|
@@ -493,16 +514,6 @@ class MpyTool():
|
|
|
493
514
|
first=False,
|
|
494
515
|
last=True)
|
|
495
516
|
|
|
496
|
-
def cmd_tree(self, dir_name):
|
|
497
|
-
tree = self._mpy.tree(dir_name)
|
|
498
|
-
self.print_tree(tree)
|
|
499
|
-
|
|
500
|
-
def cmd_get(self, *file_names):
|
|
501
|
-
for file_name in file_names:
|
|
502
|
-
self.verbose(f"GET: {file_name}", 2)
|
|
503
|
-
data = self._mpy.get(file_name)
|
|
504
|
-
print(data.decode('utf-8'))
|
|
505
|
-
|
|
506
517
|
def _upload_file(self, data, src_path, dst_path, show_progress):
|
|
507
518
|
"""Upload file data to device with stats tracking and progress display"""
|
|
508
519
|
file_size = len(data)
|
|
@@ -527,10 +538,21 @@ class MpyTool():
|
|
|
527
538
|
self._stats_wire_bytes += wire
|
|
528
539
|
return True # uploaded
|
|
529
540
|
|
|
530
|
-
def _put_dir(self, src_path, dst_path, show_progress=True):
|
|
531
|
-
|
|
541
|
+
def _put_dir(self, src_path, dst_path, show_progress=True, target_name=None):
|
|
542
|
+
"""Upload directory to device
|
|
543
|
+
|
|
544
|
+
Arguments:
|
|
545
|
+
src_path: local source directory
|
|
546
|
+
dst_path: remote destination parent directory
|
|
547
|
+
show_progress: show progress bar
|
|
548
|
+
target_name: if set, use this as directory name instead of src basename
|
|
549
|
+
"""
|
|
550
|
+
if target_name is not None:
|
|
551
|
+
basename = target_name
|
|
552
|
+
else:
|
|
553
|
+
basename = _os.path.basename(_os.path.abspath(src_path))
|
|
532
554
|
if basename:
|
|
533
|
-
dst_path =
|
|
555
|
+
dst_path = _join_remote_path(dst_path, basename)
|
|
534
556
|
self.verbose(f"PUT DIR: {src_path} -> {dst_path}", 2)
|
|
535
557
|
created_dirs = set()
|
|
536
558
|
for path, dirs, files in _os.walk(src_path, topdown=True):
|
|
@@ -565,17 +587,6 @@ class MpyTool():
|
|
|
565
587
|
raise _mpytool.MpyError(f'Error creating file under file: {parent}')
|
|
566
588
|
self._upload_file(data, src_path, dst_path, show_progress)
|
|
567
589
|
|
|
568
|
-
def cmd_put(self, src_path, dst_path):
|
|
569
|
-
if self._verbose >= 1 and not self._batch_mode:
|
|
570
|
-
self._progress_total_files = len(self._collect_local_paths(src_path))
|
|
571
|
-
self._progress_current_file = 0
|
|
572
|
-
if _os.path.isdir(src_path):
|
|
573
|
-
self._put_dir(src_path, dst_path)
|
|
574
|
-
elif _os.path.isfile(src_path):
|
|
575
|
-
self._put_file(src_path, dst_path)
|
|
576
|
-
else:
|
|
577
|
-
raise ParamsError(f'No file or directory to upload: {src_path}')
|
|
578
|
-
|
|
579
590
|
def _get_file(self, src_path, dst_path, show_progress=True):
|
|
580
591
|
"""Download single file from device"""
|
|
581
592
|
self.verbose(f"GET FILE: {src_path} -> {dst_path}", 2)
|
|
@@ -599,7 +610,7 @@ class MpyTool():
|
|
|
599
610
|
def _get_dir(self, src_path, dst_path, copy_contents=False, show_progress=True):
|
|
600
611
|
"""Download directory from device"""
|
|
601
612
|
if not copy_contents:
|
|
602
|
-
basename = src_path
|
|
613
|
+
basename = _remote_basename(src_path)
|
|
603
614
|
if basename:
|
|
604
615
|
dst_path = _os.path.join(dst_path, basename)
|
|
605
616
|
self.verbose(f"GET DIR: {src_path} -> {dst_path}", 2)
|
|
@@ -615,19 +626,28 @@ class MpyTool():
|
|
|
615
626
|
self._get_file(src_entry, dst_entry, show_progress=show_progress)
|
|
616
627
|
|
|
617
628
|
def _cp_local_to_remote(self, src_path, dst_path, dst_is_dir):
|
|
618
|
-
"""Upload local file/dir to device
|
|
619
|
-
|
|
629
|
+
"""Upload local file/dir to device
|
|
630
|
+
|
|
631
|
+
Path semantics:
|
|
632
|
+
- dst_is_dir=True: add source basename to dst_path (unless copy_contents)
|
|
633
|
+
- dst_is_dir=False: dst_path is the target name (rename)
|
|
634
|
+
- copy_contents (src ends with /): copy contents, not directory itself
|
|
635
|
+
"""
|
|
636
|
+
src_is_dir = _os.path.isdir(src_path.rstrip('/'))
|
|
620
637
|
copy_contents = src_path.endswith('/')
|
|
621
638
|
src_path = src_path.rstrip('/')
|
|
622
639
|
if not _os.path.exists(src_path):
|
|
623
640
|
raise ParamsError(f'Source not found: {src_path}')
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
641
|
+
# Normalize dst_path (remove trailing slash, but keep '/' for root)
|
|
642
|
+
if dst_path == '/':
|
|
643
|
+
pass # Keep '/' as is
|
|
644
|
+
elif dst_path:
|
|
628
645
|
dst_path = dst_path.rstrip('/')
|
|
646
|
+
else:
|
|
647
|
+
dst_path = ''
|
|
629
648
|
if src_is_dir:
|
|
630
649
|
if copy_contents:
|
|
650
|
+
# Copy directory contents to dst_path
|
|
631
651
|
for item in _os.listdir(src_path):
|
|
632
652
|
if self._is_excluded(item):
|
|
633
653
|
continue
|
|
@@ -635,10 +655,19 @@ class MpyTool():
|
|
|
635
655
|
if _os.path.isdir(item_src):
|
|
636
656
|
self._put_dir(item_src, dst_path)
|
|
637
657
|
else:
|
|
638
|
-
self._put_file(item_src, dst_path
|
|
658
|
+
self._put_file(item_src, _join_remote_path(dst_path, item))
|
|
659
|
+
elif dst_is_dir:
|
|
660
|
+
# Copy directory to destination directory (_put_dir adds basename)
|
|
661
|
+
self._put_dir(src_path, dst_path)
|
|
639
662
|
else:
|
|
640
|
-
|
|
663
|
+
# Rename: copy directory with new name
|
|
664
|
+
parent = _os.path.dirname(dst_path)
|
|
665
|
+
target_name = _os.path.basename(dst_path)
|
|
666
|
+
self._put_dir(src_path, parent, target_name=target_name)
|
|
641
667
|
else:
|
|
668
|
+
# File: add basename if dst_is_dir
|
|
669
|
+
if dst_is_dir:
|
|
670
|
+
dst_path = _join_remote_path(dst_path, _os.path.basename(src_path))
|
|
642
671
|
self._put_file(src_path, dst_path)
|
|
643
672
|
|
|
644
673
|
def _cp_remote_to_local(self, src_path, dst_path, dst_is_dir):
|
|
@@ -653,14 +682,12 @@ class MpyTool():
|
|
|
653
682
|
if not _os.path.exists(dst_path):
|
|
654
683
|
_os.makedirs(dst_path)
|
|
655
684
|
if not copy_contents and src_path != '/':
|
|
656
|
-
|
|
657
|
-
dst_path = _os.path.join(dst_path, basename)
|
|
685
|
+
dst_path = _os.path.join(dst_path, _remote_basename(src_path))
|
|
658
686
|
if src_is_dir:
|
|
659
687
|
self._get_dir(src_path, dst_path, copy_contents=copy_contents)
|
|
660
688
|
else:
|
|
661
689
|
if _os.path.isdir(dst_path):
|
|
662
|
-
|
|
663
|
-
dst_path = _os.path.join(dst_path, basename)
|
|
690
|
+
dst_path = _os.path.join(dst_path, _remote_basename(src_path))
|
|
664
691
|
self._get_file(src_path, dst_path)
|
|
665
692
|
|
|
666
693
|
def _cp_remote_to_remote(self, src_path, dst_path, dst_is_dir):
|
|
@@ -672,8 +699,7 @@ class MpyTool():
|
|
|
672
699
|
if stat == -1:
|
|
673
700
|
raise ParamsError('Remote-to-remote directory copy not supported yet')
|
|
674
701
|
if dst_is_dir:
|
|
675
|
-
|
|
676
|
-
dst_path = dst_path + basename
|
|
702
|
+
dst_path = _join_remote_path(dst_path, _remote_basename(src_path))
|
|
677
703
|
self.verbose(f"COPY: {src_path} -> {dst_path}", 2)
|
|
678
704
|
if self._verbose >= 1:
|
|
679
705
|
self._progress_current_file += 1
|
|
@@ -740,11 +766,18 @@ class MpyTool():
|
|
|
740
766
|
dest = args[-1]
|
|
741
767
|
dest_is_remote = dest.startswith(':')
|
|
742
768
|
dest_path = dest[1:] if dest_is_remote else dest
|
|
743
|
-
if
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
if
|
|
747
|
-
|
|
769
|
+
# Determine if destination is a directory:
|
|
770
|
+
# - Remote: '' (CWD) or '/' (root) or ends with '/'
|
|
771
|
+
# - Local: ends with '/' or exists as directory
|
|
772
|
+
if dest_is_remote:
|
|
773
|
+
dest_is_dir = dest_path == '' or dest_path == '/' or dest_path.endswith('/')
|
|
774
|
+
else:
|
|
775
|
+
dest_is_dir = dest_path.endswith('/') or _os.path.isdir(dest_path)
|
|
776
|
+
# Check if any source copies contents (trailing slash) or multiple sources
|
|
777
|
+
has_multi_source = len(sources) > 1
|
|
778
|
+
has_contents_copy = any(s.rstrip('/') != s for s in sources) # any source has trailing /
|
|
779
|
+
if (has_multi_source or has_contents_copy) and not dest_is_dir:
|
|
780
|
+
raise ParamsError('multiple sources or directory contents require destination directory (ending with /)')
|
|
748
781
|
if self._verbose >= 1 and not self._batch_mode:
|
|
749
782
|
total_files = 0
|
|
750
783
|
for src in sources:
|
|
@@ -766,8 +799,9 @@ class MpyTool():
|
|
|
766
799
|
copy_contents = src.endswith('/')
|
|
767
800
|
src_path = src.rstrip('/')
|
|
768
801
|
if _os.path.exists(src_path):
|
|
769
|
-
add_basename = not copy_contents
|
|
770
|
-
|
|
802
|
+
add_basename = dest_is_dir and not copy_contents
|
|
803
|
+
base_path = dest_path.rstrip('/') if dest_path else ''
|
|
804
|
+
all_dst_files.update(self._collect_dst_files(src_path, base_path, add_basename))
|
|
771
805
|
if all_dst_files and not self._batch_mode:
|
|
772
806
|
self._progress_max_dst_len = max(len(':' + p) for p in all_dst_files)
|
|
773
807
|
if not self._force:
|
|
@@ -792,50 +826,51 @@ class MpyTool():
|
|
|
792
826
|
raise ParamsError('mv requires source and destination')
|
|
793
827
|
sources = list(args[:-1])
|
|
794
828
|
dest = args[-1]
|
|
795
|
-
|
|
796
|
-
raise ParamsError('mv destination must be device path (: prefix)')
|
|
829
|
+
dest_path = _parse_device_path(dest, 'mv destination')
|
|
797
830
|
for src in sources:
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
dest_is_dir = dest_path.endswith('/')
|
|
831
|
+
_parse_device_path(src, 'mv source') # validate only
|
|
832
|
+
# ':' = CWD (empty string), ':/' = root
|
|
833
|
+
dest_is_dir = dest_path == '' or dest_path == '/' or dest_path.endswith('/')
|
|
802
834
|
if len(sources) > 1 and not dest_is_dir:
|
|
803
835
|
raise ParamsError('multiple sources require destination directory (ending with /)')
|
|
804
836
|
self._mpy.import_module('os')
|
|
805
837
|
for src in sources:
|
|
806
|
-
src_path = src
|
|
838
|
+
src_path = _parse_device_path(src, 'mv')
|
|
807
839
|
stat = self._mpy.stat(src_path)
|
|
808
840
|
if stat is None:
|
|
809
841
|
raise ParamsError(f'Source not found on device: {src_path}')
|
|
810
842
|
if dest_is_dir:
|
|
811
|
-
|
|
812
|
-
if
|
|
843
|
+
# Preserve '/' as root, strip trailing slash from others
|
|
844
|
+
dst_dir = '/' if dest_path == '/' else dest_path.rstrip('/')
|
|
845
|
+
if dst_dir and dst_dir != '/' and self._mpy.stat(dst_dir) is None:
|
|
813
846
|
self._mpy.mkdir(dst_dir)
|
|
814
|
-
|
|
815
|
-
final_dest = dest_path + basename
|
|
847
|
+
final_dest = _join_remote_path(dst_dir, _remote_basename(src_path))
|
|
816
848
|
else:
|
|
817
849
|
final_dest = dest_path
|
|
818
850
|
self.verbose(f"MV: {src_path} -> {final_dest}", 1)
|
|
819
851
|
self._mpy.rename(src_path, final_dest)
|
|
820
852
|
|
|
821
|
-
def
|
|
822
|
-
|
|
823
|
-
self.verbose(f"MKDIR: {dir_name}", 1)
|
|
824
|
-
self._mpy.mkdir(dir_name)
|
|
825
|
-
|
|
826
|
-
def cmd_delete(self, *file_names):
|
|
853
|
+
def cmd_rm(self, *file_names):
|
|
854
|
+
"""Delete files/directories on device"""
|
|
827
855
|
for file_name in file_names:
|
|
828
|
-
|
|
829
|
-
|
|
856
|
+
raw_path = _parse_device_path(file_name, 'rm')
|
|
857
|
+
contents_only = raw_path.endswith('/') or raw_path == ''
|
|
858
|
+
# ':' = CWD contents, ':/' = root, ':/path' = path, ':path/' = path contents
|
|
859
|
+
if raw_path == '':
|
|
860
|
+
path = '' # CWD
|
|
861
|
+
elif raw_path == '/':
|
|
862
|
+
path = '/' # root
|
|
863
|
+
else:
|
|
864
|
+
path = raw_path.rstrip('/') if contents_only else raw_path
|
|
830
865
|
if contents_only:
|
|
831
|
-
self.verbose(f"
|
|
866
|
+
self.verbose(f"RM contents: {path or 'CWD'}", 1)
|
|
832
867
|
entries = self._mpy.ls(path)
|
|
833
868
|
for name, size in entries:
|
|
834
|
-
entry_path = path
|
|
869
|
+
entry_path = _join_remote_path(path, name)
|
|
835
870
|
self.verbose(f" {entry_path}", 1)
|
|
836
871
|
self._mpy.delete(entry_path)
|
|
837
872
|
else:
|
|
838
|
-
self.verbose(f"
|
|
873
|
+
self.verbose(f"RM: {path}", 1)
|
|
839
874
|
self._mpy.delete(path)
|
|
840
875
|
|
|
841
876
|
def cmd_monitor(self):
|
|
@@ -861,12 +896,6 @@ class MpyTool():
|
|
|
861
896
|
terminal.run()
|
|
862
897
|
self._log.info('Exiting..')
|
|
863
898
|
|
|
864
|
-
def cmd_exec(self, code):
|
|
865
|
-
self.verbose(f"EXEC: {code}", 1)
|
|
866
|
-
result = self._mpy.comm.exec(code)
|
|
867
|
-
if result:
|
|
868
|
-
print(result.decode('utf-8', 'backslashreplace'), end='')
|
|
869
|
-
|
|
870
899
|
@staticmethod
|
|
871
900
|
def format_size(size):
|
|
872
901
|
"""Format size in bytes to human readable format (like ls -h)"""
|
|
@@ -882,28 +911,6 @@ class MpyTool():
|
|
|
882
911
|
return f"{size:.0f}{unit}"
|
|
883
912
|
return f"{size:.0f}T"
|
|
884
913
|
|
|
885
|
-
def cmd_paths(self, dir_name='.'):
|
|
886
|
-
"""Print all paths (for shell completion) - undocumented"""
|
|
887
|
-
tree = self._mpy.tree(dir_name)
|
|
888
|
-
self._print_paths(tree, '')
|
|
889
|
-
|
|
890
|
-
def _print_paths(self, entry, prefix):
|
|
891
|
-
"""Recursively print paths from tree structure"""
|
|
892
|
-
name, size, children = entry
|
|
893
|
-
if name in ('.', './'):
|
|
894
|
-
path = ''
|
|
895
|
-
else:
|
|
896
|
-
path = prefix + name
|
|
897
|
-
if children is None:
|
|
898
|
-
# File
|
|
899
|
-
print(path)
|
|
900
|
-
else:
|
|
901
|
-
# Directory
|
|
902
|
-
if path:
|
|
903
|
-
print(path + '/')
|
|
904
|
-
for child in children:
|
|
905
|
-
self._print_paths(child, path + '/' if path else '')
|
|
906
|
-
|
|
907
914
|
def cmd_ota(self, firmware_path):
|
|
908
915
|
"""OTA firmware update from local .app-bin file"""
|
|
909
916
|
self.verbose("OTA UPDATE", 1)
|
|
@@ -1095,213 +1102,272 @@ class MpyTool():
|
|
|
1095
1102
|
fs_pct = (fs['used'] / fs['total'] * 100) if fs['total'] > 0 else 0
|
|
1096
1103
|
print(f"{label:12} {self.format_size(fs['used'])} / {self.format_size(fs['total'])} ({fs_pct:.2f}%)")
|
|
1097
1104
|
|
|
1098
|
-
def
|
|
1099
|
-
"""
|
|
1100
|
-
|
|
1101
|
-
|
|
1105
|
+
def cmd_reset(self, mode='soft', reconnect=True, timeout=None):
|
|
1106
|
+
"""Reset device in specified mode
|
|
1107
|
+
|
|
1108
|
+
Modes:
|
|
1109
|
+
soft - Ctrl-D soft reset, runs boot.py/main.py (default)
|
|
1110
|
+
raw - soft reset in raw REPL, clears RAM only
|
|
1111
|
+
machine - machine.reset() with optional reconnect
|
|
1112
|
+
rts - hardware reset via DTR/RTS with optional reconnect
|
|
1113
|
+
boot - enter bootloader via machine.bootloader()
|
|
1114
|
+
dtr-boot - enter bootloader via DTR/RTS signals (ESP32)
|
|
1115
|
+
"""
|
|
1116
|
+
self.verbose(f"RESET {mode}", 1)
|
|
1117
|
+
if mode == 'soft':
|
|
1118
|
+
self._mpy.soft_reset()
|
|
1119
|
+
elif mode == 'raw':
|
|
1120
|
+
self._mpy.soft_reset_raw()
|
|
1121
|
+
elif mode == 'machine':
|
|
1122
|
+
if reconnect:
|
|
1123
|
+
try:
|
|
1124
|
+
self.verbose(" reconnecting...", 1, color='yellow')
|
|
1125
|
+
self._mpy.machine_reset(
|
|
1126
|
+
reconnect=True, timeout=timeout)
|
|
1127
|
+
self.verbose(" connected", 1, color='green')
|
|
1128
|
+
except (_mpytool.ConnError, OSError) as err:
|
|
1129
|
+
self.verbose(
|
|
1130
|
+
f" reconnect failed: {err}", 1, color='red')
|
|
1131
|
+
raise _mpytool.ConnError(
|
|
1132
|
+
f"Reconnect failed: {err}")
|
|
1133
|
+
else:
|
|
1134
|
+
self._mpy.machine_reset(reconnect=False)
|
|
1135
|
+
elif mode == 'rts':
|
|
1102
1136
|
try:
|
|
1103
|
-
self.
|
|
1104
|
-
|
|
1105
|
-
|
|
1137
|
+
self._mpy.hard_reset()
|
|
1138
|
+
if reconnect:
|
|
1139
|
+
self.verbose(
|
|
1140
|
+
" reconnecting...", 1, color='yellow')
|
|
1141
|
+
_time.sleep(1.0) # Wait for device to boot
|
|
1142
|
+
self._mpy._conn.reconnect()
|
|
1143
|
+
self.verbose(" connected", 1, color='green')
|
|
1144
|
+
except NotImplementedError:
|
|
1145
|
+
raise _mpytool.MpyError(
|
|
1146
|
+
"Hardware reset not available (serial only)")
|
|
1106
1147
|
except (_mpytool.ConnError, OSError) as err:
|
|
1107
|
-
self.verbose(
|
|
1108
|
-
|
|
1148
|
+
self.verbose(
|
|
1149
|
+
f" reconnect failed: {err}", 1, color='red')
|
|
1150
|
+
raise _mpytool.ConnError(
|
|
1151
|
+
f"Reconnect failed: {err}")
|
|
1152
|
+
elif mode == 'boot':
|
|
1153
|
+
self._mpy.machine_bootloader()
|
|
1154
|
+
elif mode == 'dtr-boot':
|
|
1155
|
+
try:
|
|
1156
|
+
self._mpy.reset_to_bootloader()
|
|
1157
|
+
except NotImplementedError:
|
|
1158
|
+
raise _mpytool.MpyError(
|
|
1159
|
+
"DTR boot not available (serial only)")
|
|
1160
|
+
|
|
1161
|
+
# -- dispatch methods for process_commands() --
|
|
1162
|
+
|
|
1163
|
+
def _dispatch_ls(self, commands, is_last_group):
|
|
1164
|
+
dir_name = ':'
|
|
1165
|
+
if commands:
|
|
1166
|
+
dir_name = commands.pop(0)
|
|
1167
|
+
# Strip trailing / except for root
|
|
1168
|
+
if dir_name not in (':', ':/'):
|
|
1169
|
+
dir_name = dir_name.rstrip('/')
|
|
1170
|
+
path = _parse_device_path(dir_name, 'ls')
|
|
1171
|
+
result = self._mpy.ls(path)
|
|
1172
|
+
for name, size in result:
|
|
1173
|
+
if size is not None:
|
|
1174
|
+
print(f'{self.format_size(size):>9} {name}')
|
|
1175
|
+
else:
|
|
1176
|
+
print(f'{"":9} {name}/')
|
|
1177
|
+
|
|
1178
|
+
def _dispatch_tree(self, commands, is_last_group):
|
|
1179
|
+
dir_name = ':'
|
|
1180
|
+
if commands:
|
|
1181
|
+
dir_name = commands.pop(0)
|
|
1182
|
+
if dir_name not in (':', ':/'):
|
|
1183
|
+
dir_name = dir_name.rstrip('/')
|
|
1184
|
+
path = _parse_device_path(dir_name, 'tree')
|
|
1185
|
+
tree = self._mpy.tree(path)
|
|
1186
|
+
self.print_tree(tree)
|
|
1187
|
+
|
|
1188
|
+
def _dispatch_cat(self, commands, is_last_group):
|
|
1189
|
+
if not commands:
|
|
1190
|
+
raise ParamsError('missing file name for cat command')
|
|
1191
|
+
for file_name in commands:
|
|
1192
|
+
path = _parse_device_path(file_name, 'cat')
|
|
1193
|
+
self.verbose(f"CAT: {path}", 2)
|
|
1194
|
+
data = self._mpy.get(path)
|
|
1195
|
+
print(data.decode('utf-8'))
|
|
1196
|
+
commands.clear()
|
|
1197
|
+
|
|
1198
|
+
def _dispatch_mkdir(self, commands, is_last_group):
|
|
1199
|
+
if not commands:
|
|
1200
|
+
raise ParamsError('missing directory name for mkdir command')
|
|
1201
|
+
for dir_name in commands:
|
|
1202
|
+
path = _parse_device_path(dir_name, 'mkdir')
|
|
1203
|
+
self.verbose(f"MKDIR: {path}", 1)
|
|
1204
|
+
self._mpy.mkdir(path)
|
|
1205
|
+
commands.clear()
|
|
1206
|
+
|
|
1207
|
+
def _dispatch_rm(self, commands, is_last_group):
|
|
1208
|
+
if not commands:
|
|
1209
|
+
raise ParamsError('missing file name for rm command')
|
|
1210
|
+
self.cmd_rm(*commands)
|
|
1211
|
+
commands.clear()
|
|
1212
|
+
|
|
1213
|
+
def _dispatch_pwd(self, commands, is_last_group):
|
|
1214
|
+
cwd = self._mpy.getcwd()
|
|
1215
|
+
print(cwd)
|
|
1216
|
+
|
|
1217
|
+
def _dispatch_cd(self, commands, is_last_group):
|
|
1218
|
+
if not commands:
|
|
1219
|
+
raise ParamsError('missing directory for cd command')
|
|
1220
|
+
dir_name = commands.pop(0)
|
|
1221
|
+
path = _parse_device_path(dir_name, 'cd')
|
|
1222
|
+
self.verbose(f"CD: {path}", 2)
|
|
1223
|
+
self._mpy.chdir(path)
|
|
1224
|
+
|
|
1225
|
+
def _dispatch_reset(self, commands, is_last_group):
|
|
1226
|
+
mode = 'soft'
|
|
1227
|
+
timeout = None
|
|
1228
|
+
_reset_modes = (
|
|
1229
|
+
'--machine', '--rts', '--raw',
|
|
1230
|
+
'--boot', '--dtr-boot')
|
|
1231
|
+
while commands and commands[0].startswith('-'):
|
|
1232
|
+
flag = commands[0]
|
|
1233
|
+
if flag in _reset_modes:
|
|
1234
|
+
mode = commands.pop(0)[2:] # strip --
|
|
1235
|
+
elif flag in ('-t', '--timeout'):
|
|
1236
|
+
commands.pop(0)
|
|
1237
|
+
if commands:
|
|
1238
|
+
timeout = int(commands.pop(0))
|
|
1239
|
+
else:
|
|
1240
|
+
raise ParamsError(
|
|
1241
|
+
'missing value for --timeout')
|
|
1242
|
+
else:
|
|
1243
|
+
raise ParamsError(
|
|
1244
|
+
f"unknown reset flag: '{flag}'")
|
|
1245
|
+
if timeout and mode not in ('machine', 'rts'):
|
|
1246
|
+
raise ParamsError(
|
|
1247
|
+
'--timeout only with --machine or --rts')
|
|
1248
|
+
has_more = bool(commands) or not is_last_group
|
|
1249
|
+
reconnect = has_more if mode in (
|
|
1250
|
+
'machine', 'rts') else True
|
|
1251
|
+
self.cmd_reset(
|
|
1252
|
+
mode=mode, reconnect=reconnect,
|
|
1253
|
+
timeout=timeout)
|
|
1254
|
+
|
|
1255
|
+
def _dispatch_monitor(self, commands, is_last_group):
|
|
1256
|
+
self.cmd_monitor()
|
|
1257
|
+
commands.clear()
|
|
1258
|
+
|
|
1259
|
+
def _dispatch_repl(self, commands, is_last_group):
|
|
1260
|
+
self.cmd_repl()
|
|
1261
|
+
commands.clear()
|
|
1262
|
+
|
|
1263
|
+
def _dispatch_exec(self, commands, is_last_group):
|
|
1264
|
+
if not commands:
|
|
1265
|
+
raise ParamsError('missing code for exec command')
|
|
1266
|
+
code = commands.pop(0)
|
|
1267
|
+
self.verbose(f"EXEC: {code}", 1)
|
|
1268
|
+
result = self._mpy.comm.exec(code)
|
|
1269
|
+
if result:
|
|
1270
|
+
print(result.decode('utf-8', 'backslashreplace'), end='')
|
|
1271
|
+
|
|
1272
|
+
def _dispatch_info(self, commands, is_last_group):
|
|
1273
|
+
self.cmd_info()
|
|
1274
|
+
|
|
1275
|
+
def _dispatch_flash(self, commands, is_last_group):
|
|
1276
|
+
if commands and commands[0] == 'read':
|
|
1277
|
+
commands.pop(0)
|
|
1278
|
+
if len(commands) >= 2:
|
|
1279
|
+
# ESP32: flash read <label> <file>
|
|
1280
|
+
label = commands.pop(0)
|
|
1281
|
+
dest_path = commands.pop(0)
|
|
1282
|
+
self.cmd_flash_read(dest_path, label=label)
|
|
1283
|
+
elif len(commands) == 1:
|
|
1284
|
+
# RP2: flash read <file>
|
|
1285
|
+
dest_path = commands.pop(0)
|
|
1286
|
+
self.cmd_flash_read(dest_path)
|
|
1287
|
+
else:
|
|
1288
|
+
raise ParamsError('flash read requires destination file')
|
|
1289
|
+
elif commands and commands[0] == 'write':
|
|
1290
|
+
commands.pop(0)
|
|
1291
|
+
if len(commands) >= 2:
|
|
1292
|
+
# ESP32: flash write <label> <file>
|
|
1293
|
+
label = commands.pop(0)
|
|
1294
|
+
src_path = commands.pop(0)
|
|
1295
|
+
self.cmd_flash_write(src_path, label=label)
|
|
1296
|
+
elif len(commands) == 1:
|
|
1297
|
+
# RP2: flash write <file>
|
|
1298
|
+
src_path = commands.pop(0)
|
|
1299
|
+
self.cmd_flash_write(src_path)
|
|
1300
|
+
else:
|
|
1301
|
+
raise ParamsError('flash write requires source file')
|
|
1302
|
+
elif commands and commands[0] == 'erase':
|
|
1303
|
+
commands.pop(0)
|
|
1304
|
+
full = False
|
|
1305
|
+
label = None
|
|
1306
|
+
while commands and (commands[0] == '--full' or not commands[0].startswith('-')):
|
|
1307
|
+
if commands[0] == '--full':
|
|
1308
|
+
full = True
|
|
1309
|
+
commands.pop(0)
|
|
1310
|
+
elif label is None:
|
|
1311
|
+
label = commands.pop(0)
|
|
1312
|
+
else:
|
|
1313
|
+
break
|
|
1314
|
+
self.cmd_flash_erase(label=label, full=full)
|
|
1109
1315
|
else:
|
|
1110
|
-
self.
|
|
1316
|
+
self.cmd_flash()
|
|
1111
1317
|
|
|
1112
|
-
def
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
self.
|
|
1318
|
+
def _dispatch_ota(self, commands, is_last_group):
|
|
1319
|
+
if not commands:
|
|
1320
|
+
raise ParamsError('ota requires firmware file path')
|
|
1321
|
+
self.cmd_ota(commands.pop(0))
|
|
1116
1322
|
|
|
1117
|
-
def
|
|
1118
|
-
|
|
1119
|
-
|
|
1323
|
+
def _dispatch_sleep(self, commands, is_last_group):
|
|
1324
|
+
if not commands:
|
|
1325
|
+
raise ParamsError('sleep requires a number (seconds)')
|
|
1120
1326
|
try:
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
self.verbose(" connected", 1, color='green')
|
|
1127
|
-
except NotImplementedError:
|
|
1128
|
-
raise _mpytool.MpyError("Hardware reset not available (serial only)")
|
|
1129
|
-
except (_mpytool.ConnError, OSError) as err:
|
|
1130
|
-
self.verbose(f" reconnect failed: {err}", 1, color='red')
|
|
1131
|
-
raise _mpytool.ConnError(f"Reconnect failed: {err}")
|
|
1327
|
+
seconds = float(commands.pop(0))
|
|
1328
|
+
except ValueError:
|
|
1329
|
+
raise ParamsError('sleep requires a number (seconds)')
|
|
1330
|
+
self.verbose(f"SLEEP {seconds}s", 1)
|
|
1331
|
+
_time.sleep(seconds)
|
|
1132
1332
|
|
|
1133
|
-
def
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
self.
|
|
1333
|
+
def _dispatch_cp(self, commands, is_last_group):
|
|
1334
|
+
if len(commands) < 2:
|
|
1335
|
+
raise ParamsError('cp requires source and destination')
|
|
1336
|
+
self.cmd_cp(*commands)
|
|
1337
|
+
commands.clear()
|
|
1137
1338
|
|
|
1138
|
-
def
|
|
1139
|
-
|
|
1339
|
+
def _dispatch_mv(self, commands, is_last_group):
|
|
1340
|
+
if len(commands) < 2:
|
|
1341
|
+
raise ParamsError('mv requires source and destination')
|
|
1342
|
+
self.cmd_mv(*commands)
|
|
1343
|
+
commands.clear()
|
|
1140
1344
|
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
self.verbose("DTRBOOT", 1)
|
|
1345
|
+
def _dispatch_paths(self, commands, is_last_group):
|
|
1346
|
+
# For shell completion
|
|
1347
|
+
dir_name = commands.pop(0) if commands else ':'
|
|
1348
|
+
path = _parse_device_path(dir_name, '_paths')
|
|
1146
1349
|
try:
|
|
1147
|
-
self._mpy.
|
|
1148
|
-
except
|
|
1149
|
-
|
|
1350
|
+
entries = self._mpy.ls(path)
|
|
1351
|
+
except (_mpytool.DirNotFound, _mpytool.MpyError):
|
|
1352
|
+
return
|
|
1353
|
+
for name, size in entries:
|
|
1354
|
+
print(name + '/' if size is None else name)
|
|
1150
1355
|
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1356
|
+
# -- command dispatching --
|
|
1357
|
+
|
|
1358
|
+
_COMMANDS = frozenset({
|
|
1359
|
+
'ls', 'tree', 'cat', 'mkdir', 'rm', 'pwd', 'cd',
|
|
1360
|
+
'reset', 'monitor', 'repl', 'exec', 'info', 'flash',
|
|
1361
|
+
'ota', 'sleep', 'cp', 'mv', '_paths',
|
|
1362
|
+
})
|
|
1155
1363
|
|
|
1156
1364
|
def process_commands(self, commands, is_last_group=False):
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
if dir_name != '/':
|
|
1164
|
-
dir_name = dir_name.rstrip('/')
|
|
1165
|
-
self.cmd_ls(dir_name)
|
|
1166
|
-
continue
|
|
1167
|
-
self.cmd_ls('.')
|
|
1168
|
-
elif command == 'tree':
|
|
1169
|
-
if commands:
|
|
1170
|
-
dir_name = commands.pop(0)
|
|
1171
|
-
if dir_name != '/':
|
|
1172
|
-
dir_name = dir_name.rstrip('/')
|
|
1173
|
-
self.cmd_tree(dir_name)
|
|
1174
|
-
continue
|
|
1175
|
-
self.cmd_tree('.')
|
|
1176
|
-
elif command in ('get', 'cat'):
|
|
1177
|
-
if commands:
|
|
1178
|
-
self.cmd_get(*commands)
|
|
1179
|
-
break
|
|
1180
|
-
raise ParamsError('missing file name for get command')
|
|
1181
|
-
elif command == 'put':
|
|
1182
|
-
if commands:
|
|
1183
|
-
src_path = commands.pop(0)
|
|
1184
|
-
dst_path = ''
|
|
1185
|
-
if commands:
|
|
1186
|
-
dst_path = commands.pop(0)
|
|
1187
|
-
self.cmd_put(src_path, dst_path)
|
|
1188
|
-
else:
|
|
1189
|
-
raise ParamsError('missing file name for put command')
|
|
1190
|
-
elif command == 'mkdir':
|
|
1191
|
-
self.cmd_mkdir(*commands)
|
|
1192
|
-
break
|
|
1193
|
-
elif command in ('del', 'delete', 'rm'):
|
|
1194
|
-
self.cmd_delete(*commands)
|
|
1195
|
-
break
|
|
1196
|
-
elif command == 'reset':
|
|
1197
|
-
self.verbose("RESET", 1)
|
|
1198
|
-
self._mpy.soft_reset()
|
|
1199
|
-
elif command == 'mreset':
|
|
1200
|
-
# Reconnect only if there are more commands (in this or next group)
|
|
1201
|
-
has_more = bool(commands) or not is_last_group
|
|
1202
|
-
self.cmd_mreset(reconnect=has_more)
|
|
1203
|
-
elif command == 'sreset':
|
|
1204
|
-
self.cmd_sreset()
|
|
1205
|
-
elif command in ('monitor', 'follow'):
|
|
1206
|
-
self.cmd_monitor()
|
|
1207
|
-
break
|
|
1208
|
-
elif command == 'repl':
|
|
1209
|
-
self.cmd_repl()
|
|
1210
|
-
break
|
|
1211
|
-
elif command == 'exec':
|
|
1212
|
-
if commands:
|
|
1213
|
-
code = commands.pop(0)
|
|
1214
|
-
self.cmd_exec(code)
|
|
1215
|
-
else:
|
|
1216
|
-
raise ParamsError('missing code for exec command')
|
|
1217
|
-
elif command == 'info':
|
|
1218
|
-
self.cmd_info()
|
|
1219
|
-
elif command == 'flash':
|
|
1220
|
-
if commands and commands[0] == 'read':
|
|
1221
|
-
commands.pop(0)
|
|
1222
|
-
if len(commands) >= 2:
|
|
1223
|
-
# ESP32: flash read <label> <file>
|
|
1224
|
-
label = commands.pop(0)
|
|
1225
|
-
dest_path = commands.pop(0)
|
|
1226
|
-
self.cmd_flash_read(dest_path, label=label)
|
|
1227
|
-
elif len(commands) == 1:
|
|
1228
|
-
# RP2: flash read <file>
|
|
1229
|
-
dest_path = commands.pop(0)
|
|
1230
|
-
self.cmd_flash_read(dest_path)
|
|
1231
|
-
else:
|
|
1232
|
-
raise ParamsError('flash read requires destination file')
|
|
1233
|
-
elif commands and commands[0] == 'write':
|
|
1234
|
-
commands.pop(0)
|
|
1235
|
-
if len(commands) >= 2:
|
|
1236
|
-
# ESP32: flash write <label> <file>
|
|
1237
|
-
label = commands.pop(0)
|
|
1238
|
-
src_path = commands.pop(0)
|
|
1239
|
-
self.cmd_flash_write(src_path, label=label)
|
|
1240
|
-
elif len(commands) == 1:
|
|
1241
|
-
# RP2: flash write <file>
|
|
1242
|
-
src_path = commands.pop(0)
|
|
1243
|
-
self.cmd_flash_write(src_path)
|
|
1244
|
-
else:
|
|
1245
|
-
raise ParamsError('flash write requires source file')
|
|
1246
|
-
elif commands and commands[0] == 'erase':
|
|
1247
|
-
commands.pop(0)
|
|
1248
|
-
# Check for --full flag and optional label
|
|
1249
|
-
full = False
|
|
1250
|
-
label = None
|
|
1251
|
-
while commands and (commands[0] == '--full' or not commands[0].startswith('-')):
|
|
1252
|
-
if commands[0] == '--full':
|
|
1253
|
-
full = True
|
|
1254
|
-
commands.pop(0)
|
|
1255
|
-
elif label is None:
|
|
1256
|
-
label = commands.pop(0)
|
|
1257
|
-
else:
|
|
1258
|
-
break
|
|
1259
|
-
self.cmd_flash_erase(label=label, full=full)
|
|
1260
|
-
else:
|
|
1261
|
-
self.cmd_flash()
|
|
1262
|
-
elif command == 'ota':
|
|
1263
|
-
if commands:
|
|
1264
|
-
firmware_path = commands.pop(0)
|
|
1265
|
-
self.cmd_ota(firmware_path)
|
|
1266
|
-
else:
|
|
1267
|
-
raise ParamsError('ota requires firmware file path')
|
|
1268
|
-
elif command == 'rtsreset':
|
|
1269
|
-
# Reconnect only if there are more commands (in this or next group)
|
|
1270
|
-
has_more = bool(commands) or not is_last_group
|
|
1271
|
-
self.cmd_rtsreset(reconnect=has_more)
|
|
1272
|
-
elif command == 'bootloader':
|
|
1273
|
-
self.cmd_bootloader()
|
|
1274
|
-
elif command == 'dtrboot':
|
|
1275
|
-
self.cmd_dtrboot()
|
|
1276
|
-
elif command == 'sleep':
|
|
1277
|
-
if commands:
|
|
1278
|
-
try:
|
|
1279
|
-
seconds = float(commands.pop(0))
|
|
1280
|
-
except ValueError:
|
|
1281
|
-
raise ParamsError('sleep requires a number (seconds)')
|
|
1282
|
-
self.cmd_sleep(seconds)
|
|
1283
|
-
else:
|
|
1284
|
-
raise ParamsError('sleep requires a number (seconds)')
|
|
1285
|
-
elif command == 'cp':
|
|
1286
|
-
if len(commands) >= 2:
|
|
1287
|
-
self.cmd_cp(*commands)
|
|
1288
|
-
break
|
|
1289
|
-
raise ParamsError('cp requires source and destination')
|
|
1290
|
-
elif command == 'mv':
|
|
1291
|
-
if len(commands) >= 2:
|
|
1292
|
-
self.cmd_mv(*commands)
|
|
1293
|
-
break
|
|
1294
|
-
raise ParamsError('mv requires source and destination')
|
|
1295
|
-
elif command == '_paths':
|
|
1296
|
-
# Undocumented: for shell completion
|
|
1297
|
-
if commands:
|
|
1298
|
-
self.cmd_paths(commands.pop(0))
|
|
1299
|
-
else:
|
|
1300
|
-
self.cmd_paths()
|
|
1301
|
-
else:
|
|
1302
|
-
raise ParamsError(f"unknown command: '{command}'")
|
|
1303
|
-
except (_mpytool.MpyError, _mpytool.ConnError) as err:
|
|
1304
|
-
self._log.error(err)
|
|
1365
|
+
while commands:
|
|
1366
|
+
command = commands.pop(0)
|
|
1367
|
+
if command not in self._COMMANDS:
|
|
1368
|
+
raise ParamsError(f"unknown command: '{command}'")
|
|
1369
|
+
dispatch = getattr(self, f'_dispatch_{command.lstrip("_")}')
|
|
1370
|
+
dispatch(commands, is_last_group)
|
|
1305
1371
|
try:
|
|
1306
1372
|
self._mpy.comm.exit_raw_repl()
|
|
1307
1373
|
except _mpytool.ConnError:
|
|
@@ -1313,38 +1379,34 @@ if _about:
|
|
|
1313
1379
|
else:
|
|
1314
1380
|
_VERSION_STR = "mpytool (not installed version)"
|
|
1315
1381
|
_COMMANDS_HELP_STR = """
|
|
1316
|
-
|
|
1317
|
-
ls [
|
|
1318
|
-
tree [
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1382
|
+
Commands (: prefix = device path, :/ = root, : = CWD):
|
|
1383
|
+
ls [:path] list files and sizes (default: CWD)
|
|
1384
|
+
tree [:path] list directory tree (default: CWD)
|
|
1385
|
+
cat {:path} [...] print file content to stdout
|
|
1386
|
+
cp [-f] {src} [...] {dst} copy files (-f = force overwrite)
|
|
1387
|
+
mv {:src} [...] {:dst} move/rename on device
|
|
1388
|
+
mkdir {:path} [...] create directory (with parents)
|
|
1389
|
+
rm {:path} [...] delete file/dir (:path/ = contents only)
|
|
1390
|
+
pwd print current working directory
|
|
1391
|
+
cd {:path} change current working directory
|
|
1392
|
+
reset [flags] soft reset (Ctrl-D) by default
|
|
1393
|
+
--machine [-t {s}] machine.reset() with reconnect
|
|
1394
|
+
--rts [-t {s}] hardware reset via DTR/RTS signal
|
|
1395
|
+
--raw soft reset in raw REPL
|
|
1396
|
+
--boot enter bootloader (machine.bootloader)
|
|
1397
|
+
--dtr-boot bootloader via DTR/RTS (ESP32)
|
|
1398
|
+
monitor print device output (Ctrl+C to stop)
|
|
1399
|
+
repl interactive REPL [Unix only]
|
|
1400
|
+
exec {code} execute Python code
|
|
1334
1401
|
info show device information
|
|
1335
|
-
flash show flash info
|
|
1402
|
+
flash show flash/partitions info
|
|
1336
1403
|
flash read [{label}] {file} read flash/partition to file
|
|
1337
1404
|
flash write [{label}] {file} write file to flash/partition
|
|
1338
1405
|
flash erase [{label}] [--full] erase flash/partition
|
|
1339
|
-
ota {firmware.app-bin} OTA
|
|
1340
|
-
sleep {seconds}
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
cat alias to get
|
|
1344
|
-
del, rm alias to delete
|
|
1345
|
-
follow alias to monitor
|
|
1346
|
-
Use -- to separate multiple commands:
|
|
1347
|
-
mpytool put main.py / -- reset -- monitor
|
|
1406
|
+
ota {firmware.app-bin} OTA update (ESP32)
|
|
1407
|
+
sleep {seconds} pause between commands
|
|
1408
|
+
Use -- to chain commands:
|
|
1409
|
+
mpytool cp main.py : -- reset -- monitor
|
|
1348
1410
|
"""
|
|
1349
1411
|
|
|
1350
1412
|
|
|
@@ -1488,11 +1550,11 @@ def main():
|
|
|
1488
1550
|
command_groups = _utils.split_commands(args.commands)
|
|
1489
1551
|
try:
|
|
1490
1552
|
_run_commands(mpy_tool, command_groups, with_progress=(args.verbose >= 1))
|
|
1553
|
+
except (_mpytool.MpyError, _mpytool.ConnError, _mpytool.Timeout) as err:
|
|
1554
|
+
log.error(err)
|
|
1491
1555
|
except KeyboardInterrupt:
|
|
1492
1556
|
# Clear partial progress line and show clean message
|
|
1493
1557
|
log.verbose('Interrupted', level=0, overwrite=True)
|
|
1494
|
-
except (_mpytool.MpyError, _mpytool.ConnError, _mpytool.Timeout) as err:
|
|
1495
|
-
log.error(err)
|
|
1496
1558
|
|
|
1497
1559
|
|
|
1498
1560
|
if __name__ == '__main__':
|
mpytool/utils.py
CHANGED
|
@@ -15,12 +15,11 @@ def parse_remote_path(path: str) -> str:
|
|
|
15
15
|
path: path with : prefix
|
|
16
16
|
|
|
17
17
|
Returns:
|
|
18
|
-
path without : prefix
|
|
18
|
+
path without : prefix (empty string for ':' means CWD)
|
|
19
19
|
"""
|
|
20
20
|
if not is_remote_path(path):
|
|
21
21
|
raise ValueError(f"Not a remote path: {path}")
|
|
22
|
-
|
|
23
|
-
return result if result else "/"
|
|
22
|
+
return path[1:]
|
|
24
23
|
|
|
25
24
|
|
|
26
25
|
def split_commands(args: list[str], separator: str = "--") -> list[list[str]]:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mpytool
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
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
|
|
@@ -83,27 +83,29 @@ $ mpytool --help
|
|
|
83
83
|
|
|
84
84
|
list files:
|
|
85
85
|
```
|
|
86
|
-
$ mpytool -p /dev/ttyACM0 ls
|
|
87
|
-
$ mpytool -p /dev/ttyACM0 ls lib
|
|
86
|
+
$ mpytool -p /dev/ttyACM0 ls # list CWD (default)
|
|
87
|
+
$ mpytool -p /dev/ttyACM0 ls :/lib # list /lib
|
|
88
88
|
```
|
|
89
89
|
|
|
90
|
-
tree
|
|
90
|
+
tree:
|
|
91
91
|
```
|
|
92
|
-
$ mpytool -p /dev/ttyACM0 tree
|
|
92
|
+
$ mpytool -p /dev/ttyACM0 tree # tree of CWD (default)
|
|
93
93
|
```
|
|
94
94
|
|
|
95
95
|
copy files (: prefix = device path):
|
|
96
96
|
```
|
|
97
97
|
$ mpytool cp main.py :/ # upload file to device root
|
|
98
98
|
$ mpytool cp main.py lib.py :/lib/ # upload multiple files to directory
|
|
99
|
-
$ mpytool cp myapp
|
|
100
|
-
$ mpytool cp myapp
|
|
99
|
+
$ mpytool cp myapp :/ # upload directory (creates :/myapp/)
|
|
100
|
+
$ mpytool cp myapp :/lib/ # upload directory into :/lib/ (creates :/lib/myapp/)
|
|
101
|
+
$ mpytool cp myapp/ :/lib/ # upload directory contents into :/lib/
|
|
101
102
|
$ mpytool cp :/main.py ./ # download file to current directory
|
|
102
103
|
$ mpytool cp :/ ./backup/ # download entire device to backup/
|
|
103
104
|
$ mpytool cp :/old.py :/new.py # copy file on device
|
|
104
105
|
$ mpytool cp -f main.py :/ # force upload even if unchanged
|
|
105
106
|
```
|
|
106
107
|
|
|
108
|
+
Path semantics: `:` = device CWD, `:/` = device root. Trailing `/` on source = copy contents only.
|
|
107
109
|
Unchanged files are automatically skipped (compares size and SHA256 hash).
|
|
108
110
|
Use `-f` or `--force` to upload all files regardless.
|
|
109
111
|
|
|
@@ -124,29 +126,43 @@ $ mpytool mv :/file.py :/lib/ # move file to directory
|
|
|
124
126
|
$ mpytool mv :/a.py :/b.py :/lib/ # move multiple files to directory
|
|
125
127
|
```
|
|
126
128
|
|
|
127
|
-
|
|
129
|
+
view file contents:
|
|
128
130
|
```
|
|
129
|
-
$ mpytool
|
|
130
|
-
$ mpytool
|
|
131
|
+
$ mpytool cat :boot.py # print file from CWD
|
|
132
|
+
$ mpytool cat :/lib/module.py # print file with absolute path
|
|
131
133
|
```
|
|
132
134
|
|
|
133
|
-
make directory, delete files:
|
|
135
|
+
make directory, delete files (: prefix = device path):
|
|
134
136
|
```
|
|
135
|
-
$ mpytool mkdir
|
|
136
|
-
$ mpytool
|
|
137
|
-
$ mpytool rm
|
|
138
|
-
$ mpytool rm
|
|
137
|
+
$ mpytool mkdir :lib :data # create directories in CWD
|
|
138
|
+
$ mpytool mkdir :/lib/subdir # create with absolute path
|
|
139
|
+
$ mpytool rm :old.py # delete file in CWD
|
|
140
|
+
$ mpytool rm :mydir # delete directory and contents
|
|
141
|
+
$ mpytool rm :mydir/ # delete contents only, keep directory
|
|
142
|
+
$ mpytool rm : # delete everything in CWD
|
|
143
|
+
$ mpytool rm :/ # delete everything on device (root)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
current working directory:
|
|
147
|
+
```
|
|
148
|
+
$ mpytool pwd # print current directory
|
|
149
|
+
/
|
|
150
|
+
$ mpytool cd :/lib # change to /lib
|
|
151
|
+
$ mpytool cd :subdir # change to relative path (from CWD)
|
|
152
|
+
$ mpytool cd :.. # change to parent directory
|
|
153
|
+
$ mpytool cd :/lib -- ls # change directory and list files
|
|
139
154
|
```
|
|
140
155
|
|
|
141
156
|
reset and REPL:
|
|
142
157
|
```
|
|
143
158
|
$ mpytool reset # soft reset (Ctrl-D, runs boot.py/main.py)
|
|
144
|
-
$ mpytool
|
|
145
|
-
$ mpytool
|
|
146
|
-
$ mpytool
|
|
147
|
-
$ mpytool
|
|
148
|
-
$ mpytool
|
|
149
|
-
$ mpytool reset
|
|
159
|
+
$ mpytool reset --raw # soft reset in raw REPL (clears RAM only)
|
|
160
|
+
$ mpytool reset --machine # MCU reset (machine.reset, auto-reconnect)
|
|
161
|
+
$ mpytool reset --machine -t 30 # MCU reset with 30s reconnect timeout
|
|
162
|
+
$ mpytool reset --rts # hardware reset via RTS signal (serial only)
|
|
163
|
+
$ mpytool reset --boot # enter bootloader (machine.bootloader)
|
|
164
|
+
$ mpytool reset --dtr-boot # enter bootloader via DTR/RTS (ESP32 only)
|
|
165
|
+
$ mpytool reset -- monitor # reset and monitor output
|
|
150
166
|
$ mpytool repl # enter REPL mode
|
|
151
167
|
$ mpytool sleep 2 # sleep for 2 seconds (useful between commands)
|
|
152
168
|
```
|
|
@@ -214,13 +230,14 @@ $ mpytool flash erase vfs --full # full erase partition
|
|
|
214
230
|
OTA firmware update (ESP32):
|
|
215
231
|
```
|
|
216
232
|
$ mpytool ota firmware.app-bin # flash to next OTA partition
|
|
217
|
-
$ mpytool ota firmware.app-bin --
|
|
233
|
+
$ mpytool ota firmware.app-bin -- reset --machine # flash and reboot
|
|
234
|
+
$ mpytool ota firmware.app-bin -- reset --machine -t 30 # flash and reboot with 30s timeout
|
|
218
235
|
```
|
|
219
236
|
|
|
220
237
|
multiple commands separated by `--`:
|
|
221
238
|
```
|
|
222
|
-
$ mpytool cp main.py boot.py
|
|
223
|
-
$ mpytool
|
|
239
|
+
$ mpytool cp main.py boot.py : -- reset -- monitor
|
|
240
|
+
$ mpytool rm :old.py -- cp new.py : -- reset
|
|
224
241
|
```
|
|
225
242
|
|
|
226
243
|
auto-detect serial port (if only one device is connected):
|
|
@@ -265,12 +282,6 @@ show version:
|
|
|
265
282
|
$ mpytool -V
|
|
266
283
|
```
|
|
267
284
|
|
|
268
|
-
Command aliases:
|
|
269
|
-
- `dir` = `ls`
|
|
270
|
-
- `cat` = `get`
|
|
271
|
-
- `del`, `rm` = `delete`
|
|
272
|
-
- `follow` = `monitor`
|
|
273
|
-
|
|
274
285
|
## Python API
|
|
275
286
|
|
|
276
287
|
```python
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
mpytool/__init__.py,sha256=4BRbfLRLpReWcDDbUA1A2UO1B8sNa6O5lgXRObbnP20,266
|
|
2
|
+
mpytool/conn.py,sha256=3WvSyeHY2qv5YT9qEEPqhSGj68wIJOXqz6G2bPCL_OM,4331
|
|
3
|
+
mpytool/conn_serial.py,sha256=AME-ry8dpBtkrkL5gp-aVxaO2Ddq5S6CmoLC5aIC7zI,4430
|
|
4
|
+
mpytool/conn_socket.py,sha256=EZh6KGMwPgNoNgl0wK4l47N9KVtaSLd3DeCTIiZ4bRw,1531
|
|
5
|
+
mpytool/logger.py,sha256=e9xaDf52nsMXU4T4Kv_pogzGq5hzP-643RxOMEDZjC0,2727
|
|
6
|
+
mpytool/mpy.py,sha256=aaHG0bg04pYKq-VU84VYR9zCSAf_TKSDXCsdhHFRKew,46360
|
|
7
|
+
mpytool/mpy_comm.py,sha256=QFHCIjdFGfDSLTVMdhAmIgz8J3QQ9VbNqzQGiaOfHKs,9149
|
|
8
|
+
mpytool/mpytool.py,sha256=ZN8YG0lKMYQZARhQHXMdfi6Z9iZJqw8Yfikv89PMLDM,65824
|
|
9
|
+
mpytool/terminal.py,sha256=YEWWwaGHISDpPWRileMPeh2drudEjF0lmjorXbrIWDY,2305
|
|
10
|
+
mpytool/utils.py,sha256=Y6DNKgXYDwnVDLOVs671XjyL7HqnsW6FMHIgCJ5MgZM,1972
|
|
11
|
+
mpytool-2.2.0.dist-info/licenses/LICENSE,sha256=wfMg5yKH2O86Digcowcm2g8Wh4zAo_gX-VqQXjmpf74,1078
|
|
12
|
+
mpytool-2.2.0.dist-info/METADATA,sha256=GXhslqFH26oFKW3TT1ksZWbWImIg-BOm-wzAfsRTrW8,14902
|
|
13
|
+
mpytool-2.2.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
14
|
+
mpytool-2.2.0.dist-info/entry_points.txt,sha256=wmnyPWEKLEbPJFR98Cc2Ky0krbMxA2BcVsS5KCTGP8Q,49
|
|
15
|
+
mpytool-2.2.0.dist-info/top_level.txt,sha256=4ZornVQbsgLIpB6d_5V1tOzX5bJvHxxDOuk8qFRjP2c,8
|
|
16
|
+
mpytool-2.2.0.dist-info/RECORD,,
|
mpytool-2.1.0.dist-info/RECORD
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
mpytool/__init__.py,sha256=4BRbfLRLpReWcDDbUA1A2UO1B8sNa6O5lgXRObbnP20,266
|
|
2
|
-
mpytool/conn.py,sha256=3WvSyeHY2qv5YT9qEEPqhSGj68wIJOXqz6G2bPCL_OM,4331
|
|
3
|
-
mpytool/conn_serial.py,sha256=dcNQ9-utloZC6AyGaj0_BkEphpJOiOzudz2OQErFG8c,4013
|
|
4
|
-
mpytool/conn_socket.py,sha256=EZh6KGMwPgNoNgl0wK4l47N9KVtaSLd3DeCTIiZ4bRw,1531
|
|
5
|
-
mpytool/logger.py,sha256=e9xaDf52nsMXU4T4Kv_pogzGq5hzP-643RxOMEDZjC0,2727
|
|
6
|
-
mpytool/mpy.py,sha256=NjQrjRh15tgAvJP--ZFrirjkS3PF4aQ-NvHi-RrHkQc,45625
|
|
7
|
-
mpytool/mpy_comm.py,sha256=QFHCIjdFGfDSLTVMdhAmIgz8J3QQ9VbNqzQGiaOfHKs,9149
|
|
8
|
-
mpytool/mpytool.py,sha256=9sRAqFwWbzbm-znaDmlsJzj8Dqg7IrQcBf_FPGVIaLA,64574
|
|
9
|
-
mpytool/terminal.py,sha256=YEWWwaGHISDpPWRileMPeh2drudEjF0lmjorXbrIWDY,2305
|
|
10
|
-
mpytool/utils.py,sha256=KbL3nDicLUT-qwP2jDl8ZCKOM1PPptZF_qxFUHYF5V8,2006
|
|
11
|
-
mpytool-2.1.0.dist-info/licenses/LICENSE,sha256=wfMg5yKH2O86Digcowcm2g8Wh4zAo_gX-VqQXjmpf74,1078
|
|
12
|
-
mpytool-2.1.0.dist-info/METADATA,sha256=4ZXTX1hUnXTnKZHKRLkGWwJwekg0P8rlfh4vIhcUjzw,13948
|
|
13
|
-
mpytool-2.1.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
14
|
-
mpytool-2.1.0.dist-info/entry_points.txt,sha256=wmnyPWEKLEbPJFR98Cc2Ky0krbMxA2BcVsS5KCTGP8Q,49
|
|
15
|
-
mpytool-2.1.0.dist-info/top_level.txt,sha256=4ZornVQbsgLIpB6d_5V1tOzX5bJvHxxDOuk8qFRjP2c,8
|
|
16
|
-
mpytool-2.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|