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/mpy_comm.py
CHANGED
|
@@ -2,13 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
import mpytool.conn as _conn
|
|
4
4
|
|
|
5
|
+
# REPL control characters
|
|
6
|
+
CTRL_A = b'\x01' # Enter raw REPL
|
|
7
|
+
CTRL_B = b'\x02' # Exit raw REPL
|
|
8
|
+
CTRL_C = b'\x03' # Interrupt
|
|
9
|
+
CTRL_D = b'\x04' # Execute / Soft reset / End raw-paste
|
|
10
|
+
CTRL_E = b'\x05' # Paste mode
|
|
11
|
+
|
|
12
|
+
# Raw-paste mode
|
|
13
|
+
RAW_PASTE_ENTER = CTRL_E + b'A' + CTRL_A # Enter raw-paste mode sequence
|
|
14
|
+
RAW_PASTE_ACK = b'\x01' # Flow control ACK
|
|
15
|
+
|
|
5
16
|
|
|
6
17
|
class MpyError(Exception):
|
|
7
18
|
"""General MPY error"""
|
|
8
19
|
|
|
9
20
|
|
|
10
21
|
class CmdError(MpyError):
|
|
11
|
-
"""
|
|
22
|
+
"""Command execution error on device"""
|
|
12
23
|
def __init__(self, cmd, result, error):
|
|
13
24
|
self._cmd = cmd
|
|
14
25
|
self._result = result
|
|
@@ -41,33 +52,61 @@ class MpyComm():
|
|
|
41
52
|
self._conn = conn
|
|
42
53
|
self._log = log
|
|
43
54
|
self._repl_mode = None
|
|
55
|
+
self._raw_paste_supported = None # None = unknown, True/False = detected
|
|
44
56
|
|
|
45
57
|
@property
|
|
46
58
|
def conn(self):
|
|
47
59
|
return self._conn
|
|
48
60
|
|
|
49
61
|
def stop_current_operation(self):
|
|
62
|
+
"""Stop any running operation and get to known REPL state.
|
|
63
|
+
|
|
64
|
+
Some USB/serial converters reset the device when port opens.
|
|
65
|
+
We send multiple Ctrl-C/Ctrl-B with short timeouts to catch the device
|
|
66
|
+
after reset completes and program starts running.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
True if we're in a known REPL state, False if recovery failed
|
|
70
|
+
"""
|
|
50
71
|
if self._repl_mode is not None:
|
|
51
|
-
return
|
|
72
|
+
return True
|
|
52
73
|
if self._log:
|
|
53
74
|
self._log.info('STOP CURRENT OPERATION')
|
|
54
|
-
self._conn.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
75
|
+
self._conn.flush()
|
|
76
|
+
|
|
77
|
+
# Try multiple attempts with short timeouts
|
|
78
|
+
# Interleave Ctrl-C (interrupt program) and Ctrl-B (exit raw REPL)
|
|
79
|
+
# This handles USB/serial converters that reset device on port open
|
|
80
|
+
for attempt in range(15):
|
|
81
|
+
# Alternate: Ctrl-C, Ctrl-C, Ctrl-B, repeat
|
|
82
|
+
if attempt % 3 == 2:
|
|
83
|
+
self._conn.write(CTRL_B)
|
|
84
|
+
else:
|
|
85
|
+
self._conn.write(CTRL_C)
|
|
86
|
+
try:
|
|
87
|
+
self._conn.read_until(b'\r\n>>> ', timeout=0.2)
|
|
88
|
+
self._repl_mode = False
|
|
89
|
+
return True
|
|
90
|
+
except _conn.Timeout:
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
if self._log:
|
|
94
|
+
self._log.warning("Could not establish REPL state")
|
|
95
|
+
return False
|
|
63
96
|
|
|
64
|
-
def enter_raw_repl(self):
|
|
97
|
+
def enter_raw_repl(self, max_retries=3):
|
|
65
98
|
if self._repl_mode is True:
|
|
66
99
|
return
|
|
67
|
-
|
|
100
|
+
retries = 0
|
|
101
|
+
while not self.stop_current_operation():
|
|
102
|
+
retries += 1
|
|
103
|
+
if retries >= max_retries:
|
|
104
|
+
raise MpyError("Could not establish REPL connection")
|
|
105
|
+
if self._log:
|
|
106
|
+
self._log.warning('..retry %d/%d', retries, max_retries)
|
|
68
107
|
if self._log:
|
|
69
108
|
self._log.info('ENTER RAW REPL')
|
|
70
|
-
self._conn.write(
|
|
109
|
+
self._conn.write(CTRL_A)
|
|
71
110
|
self._conn.read_until(b'\r\n>')
|
|
72
111
|
self._repl_mode = True
|
|
73
112
|
|
|
@@ -76,7 +115,7 @@ class MpyComm():
|
|
|
76
115
|
return
|
|
77
116
|
if self._log:
|
|
78
117
|
self._log.info('EXIT RAW REPL')
|
|
79
|
-
self._conn.write(
|
|
118
|
+
self._conn.write(CTRL_B)
|
|
80
119
|
self._conn.read_until(b'\r\n>>> ')
|
|
81
120
|
self._repl_mode = False
|
|
82
121
|
|
|
@@ -85,9 +124,21 @@ class MpyComm():
|
|
|
85
124
|
self.exit_raw_repl()
|
|
86
125
|
if self._log:
|
|
87
126
|
self._log.info('SOFT RESET')
|
|
88
|
-
self._conn.write(
|
|
127
|
+
self._conn.write(CTRL_D)
|
|
89
128
|
self._conn.read_until(b'soft reboot', timeout=1)
|
|
90
129
|
self._repl_mode = None
|
|
130
|
+
self._raw_paste_supported = None
|
|
131
|
+
|
|
132
|
+
def soft_reset_raw(self):
|
|
133
|
+
"""Soft reset in raw REPL mode - clears RAM but doesn't run boot.py/main.py"""
|
|
134
|
+
self.enter_raw_repl()
|
|
135
|
+
if self._log:
|
|
136
|
+
self._log.info('SOFT RESET (raw)')
|
|
137
|
+
self._conn.write(CTRL_D)
|
|
138
|
+
self._conn.read_until(b'soft reboot', timeout=1)
|
|
139
|
+
self._conn.read_until(b'>', timeout=1)
|
|
140
|
+
self._repl_mode = True
|
|
141
|
+
self._raw_paste_supported = None
|
|
91
142
|
|
|
92
143
|
def exec(self, command, timeout=5):
|
|
93
144
|
"""Execute command
|
|
@@ -102,18 +153,17 @@ class MpyComm():
|
|
|
102
153
|
Raises:
|
|
103
154
|
CmdError when command return error
|
|
104
155
|
"""
|
|
105
|
-
# wait for prompt
|
|
106
156
|
self.enter_raw_repl()
|
|
107
157
|
if self._log:
|
|
108
|
-
self._log.info(
|
|
158
|
+
self._log.info("CMD: %s", command)
|
|
109
159
|
self._conn.write(bytes(command, 'utf-8'))
|
|
110
|
-
self._conn.write(
|
|
160
|
+
self._conn.write(CTRL_D)
|
|
111
161
|
self._conn.read_until(b'OK', timeout)
|
|
112
|
-
result = self._conn.read_until(
|
|
162
|
+
result = self._conn.read_until(CTRL_D, timeout)
|
|
113
163
|
if result:
|
|
114
164
|
if self._log:
|
|
115
|
-
self._log.info(
|
|
116
|
-
err = self._conn.read_until(b'
|
|
165
|
+
self._log.info('RES: %s', bytes(result))
|
|
166
|
+
err = self._conn.read_until(CTRL_D + b'>', timeout)
|
|
117
167
|
if err:
|
|
118
168
|
raise CmdError(command, result, err)
|
|
119
169
|
return result
|
|
@@ -121,3 +171,111 @@ class MpyComm():
|
|
|
121
171
|
def exec_eval(self, command, timeout=5):
|
|
122
172
|
result = self.exec(f'print({command})', timeout)
|
|
123
173
|
return eval(result)
|
|
174
|
+
|
|
175
|
+
def exec_raw_paste(self, command, timeout=5):
|
|
176
|
+
"""Execute command using raw-paste mode with flow control.
|
|
177
|
+
|
|
178
|
+
Raw-paste mode compiles code as it receives it, using less RAM
|
|
179
|
+
and providing better reliability for large code transfers.
|
|
180
|
+
|
|
181
|
+
Arguments:
|
|
182
|
+
command: command to execute (str or bytes)
|
|
183
|
+
timeout: maximum waiting time for result
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
command STDOUT result
|
|
187
|
+
|
|
188
|
+
Raises:
|
|
189
|
+
CmdError when command returns error
|
|
190
|
+
MpyError when raw-paste mode is not supported
|
|
191
|
+
"""
|
|
192
|
+
self.enter_raw_repl()
|
|
193
|
+
|
|
194
|
+
if isinstance(command, str):
|
|
195
|
+
command = command.encode('utf-8')
|
|
196
|
+
|
|
197
|
+
if self._log:
|
|
198
|
+
self._log.info("CMD (raw-paste, %d bytes)", len(command))
|
|
199
|
+
|
|
200
|
+
self._conn.write(RAW_PASTE_ENTER)
|
|
201
|
+
|
|
202
|
+
# Read response: 'R' + status (0=not supported, 1=supported)
|
|
203
|
+
header, status = self._conn.read_bytes(2, timeout)
|
|
204
|
+
if header != ord('R'):
|
|
205
|
+
raise MpyError(f"Unexpected raw-paste header: {header!r}")
|
|
206
|
+
|
|
207
|
+
if status == 0:
|
|
208
|
+
self._raw_paste_supported = False
|
|
209
|
+
raise MpyError("Raw-paste mode not supported by device")
|
|
210
|
+
|
|
211
|
+
if status != 1:
|
|
212
|
+
raise MpyError(f"Unexpected raw-paste status: {status}")
|
|
213
|
+
|
|
214
|
+
self._raw_paste_supported = True
|
|
215
|
+
|
|
216
|
+
# Read window size (16-bit little-endian)
|
|
217
|
+
window_bytes = self._conn.read_bytes(2, timeout)
|
|
218
|
+
window_size = int.from_bytes(window_bytes, 'little')
|
|
219
|
+
if self._log:
|
|
220
|
+
self._log.info("Raw-paste window size: %d", window_size)
|
|
221
|
+
|
|
222
|
+
# Send data with flow control
|
|
223
|
+
remaining_window = window_size
|
|
224
|
+
offset = 0
|
|
225
|
+
|
|
226
|
+
while offset < len(command):
|
|
227
|
+
# Check for incoming flow control byte (non-blocking)
|
|
228
|
+
if remaining_window == 0 or self._conn._has_data(0):
|
|
229
|
+
flow_byte = self._conn.read_bytes(1, timeout)
|
|
230
|
+
if flow_byte == RAW_PASTE_ACK:
|
|
231
|
+
remaining_window += window_size
|
|
232
|
+
elif flow_byte == CTRL_D:
|
|
233
|
+
# Device wants to abort - syntax error during compilation
|
|
234
|
+
self._conn.write(CTRL_D)
|
|
235
|
+
break
|
|
236
|
+
|
|
237
|
+
# Send data up to remaining window size
|
|
238
|
+
if remaining_window > 0:
|
|
239
|
+
chunk_size = min(remaining_window, len(command) - offset)
|
|
240
|
+
self._conn.write(command[offset:offset + chunk_size])
|
|
241
|
+
offset += chunk_size
|
|
242
|
+
remaining_window -= chunk_size
|
|
243
|
+
|
|
244
|
+
self._conn.write(CTRL_D)
|
|
245
|
+
|
|
246
|
+
# Consume remaining ACKs and wait for CTRL_D echo
|
|
247
|
+
while True:
|
|
248
|
+
byte = self._conn.read_bytes(1, timeout)
|
|
249
|
+
if byte == CTRL_D:
|
|
250
|
+
break
|
|
251
|
+
|
|
252
|
+
result = self._conn.read_until(CTRL_D, timeout)
|
|
253
|
+
if result and self._log:
|
|
254
|
+
self._log.info('RES: %s', bytes(result))
|
|
255
|
+
err = self._conn.read_until(CTRL_D + b'>', timeout)
|
|
256
|
+
if err:
|
|
257
|
+
raise CmdError(command.decode('utf-8', errors='replace'), result, err)
|
|
258
|
+
return result
|
|
259
|
+
|
|
260
|
+
def try_raw_paste(self, command, timeout=5):
|
|
261
|
+
"""Try raw-paste mode, fall back to regular exec if not supported.
|
|
262
|
+
|
|
263
|
+
Arguments:
|
|
264
|
+
command: command to execute
|
|
265
|
+
timeout: maximum waiting time for result
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
command STDOUT result
|
|
269
|
+
"""
|
|
270
|
+
# If we know raw-paste is not supported, skip it
|
|
271
|
+
if self._raw_paste_supported is False:
|
|
272
|
+
return self.exec(command, timeout)
|
|
273
|
+
|
|
274
|
+
try:
|
|
275
|
+
return self.exec_raw_paste(command, timeout)
|
|
276
|
+
except MpyError as e:
|
|
277
|
+
if "not supported" in str(e):
|
|
278
|
+
if self._log:
|
|
279
|
+
self._log.info("Raw-paste not supported, using regular exec")
|
|
280
|
+
return self.exec(command, timeout)
|
|
281
|
+
raise
|