mpytool 1.2.0__py3-none-any.whl → 2.1.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/__init__.py +2 -0
- mpytool/conn.py +102 -11
- mpytool/conn_serial.py +90 -46
- mpytool/conn_socket.py +53 -0
- mpytool/logger.py +87 -0
- mpytool/mpy.py +1097 -70
- mpytool/mpy_comm.py +180 -22
- mpytool/mpytool.py +1303 -132
- mpytool/terminal.py +42 -13
- mpytool/utils.py +83 -0
- mpytool-2.1.0.dist-info/METADATA +451 -0
- mpytool-2.1.0.dist-info/RECORD +16 -0
- {mpytool-1.2.0.dist-info → mpytool-2.1.0.dist-info}/WHEEL +1 -1
- {mpytool-1.2.0.dist-info → mpytool-2.1.0.dist-info}/entry_points.txt +0 -1
- mpytool/__about__.py +0 -12
- mpytool-1.2.0.dist-info/METADATA +0 -23
- mpytool-1.2.0.dist-info/RECORD +0 -14
- {mpytool-1.2.0.dist-info → mpytool-2.1.0.dist-info/licenses}/LICENSE +0 -0
- {mpytool-1.2.0.dist-info → mpytool-2.1.0.dist-info}/top_level.txt +0 -0
mpytool/__init__.py
CHANGED
mpytool/conn.py
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
"""MicroPython tool: abstract connector"""
|
|
2
2
|
|
|
3
|
+
import time as _time
|
|
4
|
+
import select as _select
|
|
5
|
+
|
|
3
6
|
|
|
4
7
|
class ConnError(Exception):
|
|
5
8
|
"""General connection error"""
|
|
@@ -12,27 +15,115 @@ class Timeout(ConnError):
|
|
|
12
15
|
class Conn():
|
|
13
16
|
def __init__(self, log=None):
|
|
14
17
|
self._log = log
|
|
18
|
+
self._buffer = bytearray(b'')
|
|
15
19
|
|
|
16
20
|
@property
|
|
17
21
|
def fd(self):
|
|
18
|
-
"""Return file descriptor
|
|
19
|
-
"""
|
|
22
|
+
"""Return file descriptor for select()"""
|
|
20
23
|
return None
|
|
21
24
|
|
|
22
|
-
def
|
|
23
|
-
"""
|
|
24
|
-
|
|
25
|
+
def _has_data(self, timeout=0):
|
|
26
|
+
"""Check if data is available to read using select()"""
|
|
27
|
+
fd = self.fd
|
|
28
|
+
if fd is None:
|
|
29
|
+
return False
|
|
30
|
+
readable, _, _ = _select.select([fd], [], [], timeout)
|
|
31
|
+
return bool(readable)
|
|
32
|
+
|
|
33
|
+
def _read_available(self):
|
|
34
|
+
"""Read available data from device (must be implemented by subclass)"""
|
|
35
|
+
raise NotImplementedError
|
|
25
36
|
|
|
26
|
-
def
|
|
27
|
-
"""Write to device
|
|
37
|
+
def _write_raw(self, data):
|
|
38
|
+
"""Write data to device, return bytes written (must be implemented by subclass)"""
|
|
39
|
+
raise NotImplementedError
|
|
40
|
+
|
|
41
|
+
def _read_to_buffer(self, wait_timeout=0):
|
|
42
|
+
"""Read available data into buffer
|
|
43
|
+
|
|
44
|
+
Arguments:
|
|
45
|
+
wait_timeout: how long to wait for data (0 = non-blocking)
|
|
28
46
|
"""
|
|
47
|
+
if self._has_data(wait_timeout):
|
|
48
|
+
data = self._read_available()
|
|
49
|
+
if data:
|
|
50
|
+
self._buffer += data
|
|
51
|
+
return True
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
def flush(self):
|
|
55
|
+
"""Flush and return buffer contents"""
|
|
56
|
+
buffer = bytes(self._buffer)
|
|
57
|
+
del self._buffer[:]
|
|
58
|
+
return buffer
|
|
59
|
+
|
|
60
|
+
def read(self):
|
|
61
|
+
"""Read available data from device"""
|
|
62
|
+
if self._has_data():
|
|
63
|
+
return self._read_available()
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
def read_bytes(self, count, timeout=1):
|
|
67
|
+
"""Read exactly count bytes from device"""
|
|
68
|
+
start_time = _time.time()
|
|
69
|
+
while len(self._buffer) < count:
|
|
70
|
+
if self._read_to_buffer(wait_timeout=0.001):
|
|
71
|
+
start_time = _time.time()
|
|
72
|
+
if timeout is not None and start_time + timeout < _time.time():
|
|
73
|
+
if self._buffer:
|
|
74
|
+
raise Timeout(
|
|
75
|
+
f"During timeout received: {bytes(self._buffer)}")
|
|
76
|
+
raise Timeout("No data received")
|
|
77
|
+
data = bytes(self._buffer[:count])
|
|
78
|
+
del self._buffer[:count]
|
|
79
|
+
if self._log:
|
|
80
|
+
self._log.debug("rd: %s", data)
|
|
81
|
+
return data
|
|
82
|
+
|
|
83
|
+
def write(self, data):
|
|
84
|
+
"""Write data to device"""
|
|
85
|
+
if self._log:
|
|
86
|
+
self._log.debug("wr: %s", bytes(data))
|
|
87
|
+
while data:
|
|
88
|
+
count = self._write_raw(data)
|
|
89
|
+
data = data[count:]
|
|
29
90
|
|
|
30
91
|
def read_until(self, end, timeout=1):
|
|
31
|
-
"""Read until
|
|
32
|
-
|
|
33
|
-
|
|
92
|
+
"""Read until end marker is found"""
|
|
93
|
+
if self._log:
|
|
94
|
+
self._log.debug("wait for %s", end)
|
|
95
|
+
start_time = _time.time()
|
|
96
|
+
while True:
|
|
97
|
+
# Use select() with 1ms timeout instead of sleep - wakes immediately on data
|
|
98
|
+
if self._read_to_buffer(wait_timeout=0.001):
|
|
99
|
+
start_time = _time.time() # reset timeout on data received
|
|
100
|
+
if end in self._buffer:
|
|
101
|
+
break
|
|
102
|
+
if timeout is not None and start_time + timeout < _time.time():
|
|
103
|
+
if self._buffer:
|
|
104
|
+
raise Timeout(
|
|
105
|
+
f"During timeout received: {bytes(self._buffer)}")
|
|
106
|
+
raise Timeout("No data received")
|
|
107
|
+
index = self._buffer.index(end)
|
|
108
|
+
data = self._buffer[:index]
|
|
109
|
+
del self._buffer[:index + len(end)]
|
|
110
|
+
if self._log:
|
|
111
|
+
self._log.debug("rd: %s", bytes(data + end))
|
|
112
|
+
return data
|
|
34
113
|
|
|
35
114
|
def read_line(self, timeout=None):
|
|
36
|
-
"""Read
|
|
115
|
+
"""Read single line"""
|
|
37
116
|
line = self.read_until(b'\n', timeout)
|
|
38
117
|
return line.strip(b'\r')
|
|
118
|
+
|
|
119
|
+
def hard_reset(self):
|
|
120
|
+
"""Hardware reset (only available on serial connections)"""
|
|
121
|
+
raise NotImplementedError("Hardware reset not available on this connection")
|
|
122
|
+
|
|
123
|
+
def reset_to_bootloader(self):
|
|
124
|
+
"""Reset into bootloader mode (only available on serial connections)"""
|
|
125
|
+
raise NotImplementedError("Reset to bootloader not available on this connection")
|
|
126
|
+
|
|
127
|
+
def reconnect(self, timeout=None):
|
|
128
|
+
"""Reconnect after device reset (only available on serial connections)"""
|
|
129
|
+
raise NotImplementedError("Reconnect not available on this connection")
|
mpytool/conn_serial.py
CHANGED
|
@@ -6,62 +6,106 @@ import mpytool.conn as _conn
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class ConnSerial(_conn.Conn):
|
|
9
|
+
RECONNECT_TIMEOUT = 5 # seconds
|
|
10
|
+
|
|
9
11
|
def __init__(self, log=None, **serial_config):
|
|
10
12
|
super().__init__(log)
|
|
11
|
-
self.
|
|
12
|
-
|
|
13
|
+
self._serial_config = serial_config
|
|
14
|
+
try:
|
|
15
|
+
self._serial = _serial.Serial(**serial_config)
|
|
16
|
+
except _serial.serialutil.SerialException as err:
|
|
17
|
+
self._serial = None
|
|
18
|
+
raise _conn.ConnError(
|
|
19
|
+
f"Error opening serial port {serial_config['port']}") from err
|
|
13
20
|
|
|
14
21
|
def __del__(self):
|
|
15
22
|
if self._serial:
|
|
16
23
|
self._serial.close()
|
|
17
24
|
|
|
18
|
-
def _read_to_buffer(self):
|
|
19
|
-
in_waiting = self._serial.in_waiting
|
|
20
|
-
if in_waiting > 0:
|
|
21
|
-
self._buffer += self._serial.read(in_waiting)
|
|
22
|
-
return True
|
|
23
|
-
return False
|
|
24
|
-
|
|
25
25
|
@property
|
|
26
26
|
def fd(self):
|
|
27
|
-
return self._serial.fd
|
|
27
|
+
return self._serial.fd if self._serial else None
|
|
28
28
|
|
|
29
|
-
def
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return data
|
|
29
|
+
def _read_available(self):
|
|
30
|
+
"""Read available data from serial port"""
|
|
31
|
+
in_waiting = self._serial.in_waiting
|
|
32
|
+
if in_waiting > 0:
|
|
33
|
+
return self._serial.read(in_waiting)
|
|
35
34
|
return None
|
|
36
35
|
|
|
37
|
-
def
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
36
|
+
def _write_raw(self, data):
|
|
37
|
+
"""Write data to serial port"""
|
|
38
|
+
return self._serial.write(data)
|
|
39
|
+
|
|
40
|
+
def _is_usb_cdc(self):
|
|
41
|
+
"""Check if this is a USB-CDC port (native USB on ESP32-S/C)"""
|
|
42
|
+
port = self._serial.port or ''
|
|
43
|
+
return 'ACM' in port or 'usbmodem' in port
|
|
44
|
+
|
|
45
|
+
def hard_reset(self):
|
|
46
|
+
"""Hardware reset using DTR/RTS signals"""
|
|
47
|
+
self._serial.setDTR(False) # GPIO0 high (normal boot)
|
|
48
|
+
self._serial.setRTS(True) # Assert reset
|
|
49
|
+
_time.sleep(0.1)
|
|
50
|
+
self._serial.setRTS(False) # Release reset
|
|
51
|
+
|
|
52
|
+
def reconnect(self, timeout=None):
|
|
53
|
+
"""Close and reopen serial port (for USB-CDC reconnect after reset)"""
|
|
54
|
+
if timeout is None:
|
|
55
|
+
timeout = self.RECONNECT_TIMEOUT
|
|
56
|
+
port = self._serial_config.get('port', 'unknown')
|
|
57
|
+
if self._serial:
|
|
58
|
+
try:
|
|
59
|
+
self._serial.close()
|
|
60
|
+
except (OSError, _serial.serialutil.SerialException):
|
|
61
|
+
pass # Port may already be gone
|
|
62
|
+
self._serial = None
|
|
63
|
+
start = _time.time()
|
|
64
|
+
while _time.time() - start < timeout:
|
|
65
|
+
try:
|
|
66
|
+
self._serial = _serial.Serial(**self._serial_config)
|
|
67
|
+
del self._buffer[:] # Clear any stale data
|
|
68
|
+
_time.sleep(0.5) # Wait for device to stabilize
|
|
69
|
+
# Verify connection is stable
|
|
70
|
+
_ = self._serial.in_waiting
|
|
71
|
+
return True
|
|
72
|
+
except (_serial.serialutil.SerialException, OSError):
|
|
73
|
+
if self._serial:
|
|
74
|
+
try:
|
|
75
|
+
self._serial.close()
|
|
76
|
+
except (OSError, _serial.serialutil.SerialException):
|
|
77
|
+
pass
|
|
78
|
+
self._serial = None
|
|
79
|
+
_time.sleep(0.1)
|
|
80
|
+
raise _conn.ConnError(f"Could not reconnect to {port} within {timeout}s")
|
|
81
|
+
|
|
82
|
+
def reset_to_bootloader(self):
|
|
83
|
+
"""Reset into bootloader mode (auto-detects USB-CDC vs USB-UART)"""
|
|
84
|
+
if self._is_usb_cdc():
|
|
85
|
+
self._reset_to_bootloader_usb_jtag()
|
|
86
|
+
else:
|
|
87
|
+
self._reset_to_bootloader_classic()
|
|
88
|
+
|
|
89
|
+
def _reset_to_bootloader_usb_jtag(self):
|
|
90
|
+
"""Bootloader reset for USB-JTAG-Serial (esptool sequence)"""
|
|
91
|
+
self._serial.setRTS(False)
|
|
92
|
+
self._serial.setDTR(False)
|
|
93
|
+
_time.sleep(0.1)
|
|
94
|
+
self._serial.setDTR(True)
|
|
95
|
+
self._serial.setRTS(True)
|
|
96
|
+
_time.sleep(0.1)
|
|
97
|
+
self._serial.setDTR(False)
|
|
98
|
+
self._serial.setRTS(True)
|
|
99
|
+
_time.sleep(0.1)
|
|
100
|
+
self._serial.setDTR(False)
|
|
101
|
+
self._serial.setRTS(False)
|
|
45
102
|
|
|
46
|
-
def
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if timeout is not None and start_time + timeout < _time.time():
|
|
56
|
-
if self._buffer:
|
|
57
|
-
raise _conn.Timeout(
|
|
58
|
-
f"During timeout received: {bytes(self._buffer)}")
|
|
59
|
-
raise _conn.Timeout("No data received")
|
|
60
|
-
_time.sleep(.01)
|
|
61
|
-
index = self._buffer.index(end)
|
|
62
|
-
data = self._buffer[:index]
|
|
63
|
-
del self._buffer[:index + len(end)]
|
|
64
|
-
# data, self._buffer = self._buffer.split(end, 1)
|
|
65
|
-
if self._log:
|
|
66
|
-
self._log.debug(f"rd: {data + end}")
|
|
67
|
-
return data
|
|
103
|
+
def _reset_to_bootloader_classic(self):
|
|
104
|
+
"""Bootloader reset for USB-UART (classic DTR/RTS circuit)"""
|
|
105
|
+
self._serial.setDTR(False) # GPIO0 high
|
|
106
|
+
self._serial.setRTS(True) # Assert reset
|
|
107
|
+
_time.sleep(0.1)
|
|
108
|
+
self._serial.setDTR(True) # GPIO0 low (bootloader)
|
|
109
|
+
self._serial.setRTS(False) # Release reset
|
|
110
|
+
_time.sleep(0.05)
|
|
111
|
+
self._serial.setDTR(False) # GPIO0 high
|
mpytool/conn_socket.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""MicroPython tool: socket connector"""
|
|
2
|
+
|
|
3
|
+
import socket as _socket
|
|
4
|
+
import mpytool.conn as _conn
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ConnSocket(_conn.Conn):
|
|
8
|
+
def __init__(self, address, log=None):
|
|
9
|
+
super().__init__(log)
|
|
10
|
+
self._socket = None
|
|
11
|
+
sock = _socket.socket(_socket.AF_INET, _socket.SOCK_STREAM)
|
|
12
|
+
sock.settimeout(5)
|
|
13
|
+
if ':' in address:
|
|
14
|
+
host, port = address.split(':')
|
|
15
|
+
port = int(port)
|
|
16
|
+
else:
|
|
17
|
+
host = address
|
|
18
|
+
port = 23
|
|
19
|
+
if log:
|
|
20
|
+
log.info(f"Connecting to: {host}:{port}")
|
|
21
|
+
try:
|
|
22
|
+
sock.connect((host, port))
|
|
23
|
+
except _socket.timeout as err:
|
|
24
|
+
raise _conn.ConnError(f"Timeout connecting to {host}:{port}") from err
|
|
25
|
+
except _socket.error as err:
|
|
26
|
+
raise _conn.ConnError(f"Cannot connect to {host}:{port}: {err}") from err
|
|
27
|
+
sock.settimeout(None)
|
|
28
|
+
sock.setblocking(False)
|
|
29
|
+
self._socket = sock
|
|
30
|
+
if log:
|
|
31
|
+
log.info("connected")
|
|
32
|
+
|
|
33
|
+
def __del__(self):
|
|
34
|
+
if self._socket:
|
|
35
|
+
self._socket.close()
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def fd(self):
|
|
39
|
+
return self._socket.fileno() if self._socket else None
|
|
40
|
+
|
|
41
|
+
def _read_available(self):
|
|
42
|
+
"""Read available data from socket"""
|
|
43
|
+
try:
|
|
44
|
+
data = self._socket.recv(4096)
|
|
45
|
+
if data:
|
|
46
|
+
return data
|
|
47
|
+
except BlockingIOError:
|
|
48
|
+
pass
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
def _write_raw(self, data):
|
|
52
|
+
"""Write data to socket"""
|
|
53
|
+
return self._socket.send(data)
|
mpytool/logger.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Simple color logger for mpytool"""
|
|
2
|
+
|
|
3
|
+
import os as _os
|
|
4
|
+
import sys as _sys
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SimpleColorLogger():
|
|
8
|
+
# ANSI color codes
|
|
9
|
+
_RESET = '\033[0m'
|
|
10
|
+
_BOLD_RED = '\033[1;31m'
|
|
11
|
+
_BOLD_YELLOW = '\033[1;33m'
|
|
12
|
+
_BOLD_MAGENTA = '\033[1;35m'
|
|
13
|
+
_BOLD_BLUE = '\033[1;34m'
|
|
14
|
+
_BOLD_GREEN = '\033[1;32m'
|
|
15
|
+
_BOLD_CYAN = '\033[1;36m'
|
|
16
|
+
_CLEAR_LINE = '\033[K'
|
|
17
|
+
|
|
18
|
+
# Color names for verbose()
|
|
19
|
+
COLORS = {
|
|
20
|
+
'red': _BOLD_RED,
|
|
21
|
+
'yellow': _BOLD_YELLOW,
|
|
22
|
+
'magenta': _BOLD_MAGENTA,
|
|
23
|
+
'blue': _BOLD_BLUE,
|
|
24
|
+
'green': _BOLD_GREEN,
|
|
25
|
+
'cyan': _BOLD_CYAN,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
def __init__(self, loglevel=1, verbose_level=0):
|
|
29
|
+
self._loglevel = loglevel
|
|
30
|
+
self._verbose_level = verbose_level
|
|
31
|
+
self._is_tty = (
|
|
32
|
+
_sys.stderr.isatty()
|
|
33
|
+
and _os.environ.get('NO_COLOR') is None
|
|
34
|
+
and _os.environ.get('TERM') != 'dumb'
|
|
35
|
+
and _os.environ.get('CI') is None
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
def log(self, msg):
|
|
39
|
+
print(msg, file=_sys.stderr)
|
|
40
|
+
|
|
41
|
+
def error(self, msg, *args):
|
|
42
|
+
if args:
|
|
43
|
+
msg = msg % args
|
|
44
|
+
if self._loglevel >= 1:
|
|
45
|
+
if self._is_tty:
|
|
46
|
+
self.log(f"{self._BOLD_RED}{msg}{self._RESET}")
|
|
47
|
+
else:
|
|
48
|
+
self.log(f"E: {msg}")
|
|
49
|
+
|
|
50
|
+
def warning(self, msg, *args):
|
|
51
|
+
if args:
|
|
52
|
+
msg = msg % args
|
|
53
|
+
if self._loglevel >= 2:
|
|
54
|
+
if self._is_tty:
|
|
55
|
+
self.log(f"{self._BOLD_YELLOW}{msg}{self._RESET}")
|
|
56
|
+
else:
|
|
57
|
+
self.log(f"W: {msg}")
|
|
58
|
+
|
|
59
|
+
def info(self, msg, *args):
|
|
60
|
+
if args:
|
|
61
|
+
msg = msg % args
|
|
62
|
+
if self._loglevel >= 3:
|
|
63
|
+
if self._is_tty:
|
|
64
|
+
self.log(f"{self._BOLD_MAGENTA}{msg}{self._RESET}")
|
|
65
|
+
else:
|
|
66
|
+
self.log(f"I: {msg}")
|
|
67
|
+
|
|
68
|
+
def debug(self, msg, *args):
|
|
69
|
+
if args:
|
|
70
|
+
msg = msg % args
|
|
71
|
+
if self._loglevel >= 4:
|
|
72
|
+
if self._is_tty:
|
|
73
|
+
self.log(f"{self._BOLD_BLUE}{msg}{self._RESET}")
|
|
74
|
+
else:
|
|
75
|
+
self.log(f"D: {msg}")
|
|
76
|
+
|
|
77
|
+
def verbose(self, msg, level=1, color='green', end='\n', overwrite=False):
|
|
78
|
+
"""Print verbose message if verbose_level >= level"""
|
|
79
|
+
if self._verbose_level < level:
|
|
80
|
+
return
|
|
81
|
+
# Skip progress updates (overwrite without newline) in non-TTY mode
|
|
82
|
+
if overwrite and not self._is_tty and end != '\n':
|
|
83
|
+
return
|
|
84
|
+
color_code = self.COLORS.get(color, self._BOLD_GREEN) if self._is_tty else ''
|
|
85
|
+
reset_code = self._RESET if self._is_tty else ''
|
|
86
|
+
clear = f'\r{self._CLEAR_LINE}' if self._is_tty and overwrite else ''
|
|
87
|
+
print(f"{clear}{color_code}{msg}{reset_code}", end=end, file=_sys.stderr, flush=True)
|