mpytool 2.0.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/conn.py +29 -0
- mpytool/conn_serial.py +77 -0
- mpytool/mpy.py +1040 -78
- mpytool/mpy_comm.py +140 -11
- mpytool/mpytool.py +820 -234
- mpytool/terminal.py +1 -1
- mpytool/utils.py +4 -3
- mpytool-2.1.0.dist-info/METADATA +451 -0
- mpytool-2.1.0.dist-info/RECORD +16 -0
- {mpytool-2.0.0.dist-info → mpytool-2.1.0.dist-info}/WHEEL +1 -1
- mpytool-2.0.0.dist-info/METADATA +0 -233
- mpytool-2.0.0.dist-info/RECORD +0 -16
- {mpytool-2.0.0.dist-info → mpytool-2.1.0.dist-info}/entry_points.txt +0 -0
- {mpytool-2.0.0.dist-info → mpytool-2.1.0.dist-info}/licenses/LICENSE +0 -0
- {mpytool-2.0.0.dist-info → mpytool-2.1.0.dist-info}/top_level.txt +0 -0
mpytool/mpy_comm.py
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
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"""
|
|
@@ -41,6 +52,7 @@ 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):
|
|
@@ -60,8 +72,6 @@ class MpyComm():
|
|
|
60
72
|
return True
|
|
61
73
|
if self._log:
|
|
62
74
|
self._log.info('STOP CURRENT OPERATION')
|
|
63
|
-
|
|
64
|
-
# Flush any pending data first
|
|
65
75
|
self._conn.flush()
|
|
66
76
|
|
|
67
77
|
# Try multiple attempts with short timeouts
|
|
@@ -70,9 +80,9 @@ class MpyComm():
|
|
|
70
80
|
for attempt in range(15):
|
|
71
81
|
# Alternate: Ctrl-C, Ctrl-C, Ctrl-B, repeat
|
|
72
82
|
if attempt % 3 == 2:
|
|
73
|
-
self._conn.write(
|
|
83
|
+
self._conn.write(CTRL_B)
|
|
74
84
|
else:
|
|
75
|
-
self._conn.write(
|
|
85
|
+
self._conn.write(CTRL_C)
|
|
76
86
|
try:
|
|
77
87
|
self._conn.read_until(b'\r\n>>> ', timeout=0.2)
|
|
78
88
|
self._repl_mode = False
|
|
@@ -96,7 +106,7 @@ class MpyComm():
|
|
|
96
106
|
self._log.warning('..retry %d/%d', retries, max_retries)
|
|
97
107
|
if self._log:
|
|
98
108
|
self._log.info('ENTER RAW REPL')
|
|
99
|
-
self._conn.write(
|
|
109
|
+
self._conn.write(CTRL_A)
|
|
100
110
|
self._conn.read_until(b'\r\n>')
|
|
101
111
|
self._repl_mode = True
|
|
102
112
|
|
|
@@ -105,7 +115,7 @@ class MpyComm():
|
|
|
105
115
|
return
|
|
106
116
|
if self._log:
|
|
107
117
|
self._log.info('EXIT RAW REPL')
|
|
108
|
-
self._conn.write(
|
|
118
|
+
self._conn.write(CTRL_B)
|
|
109
119
|
self._conn.read_until(b'\r\n>>> ')
|
|
110
120
|
self._repl_mode = False
|
|
111
121
|
|
|
@@ -114,9 +124,21 @@ class MpyComm():
|
|
|
114
124
|
self.exit_raw_repl()
|
|
115
125
|
if self._log:
|
|
116
126
|
self._log.info('SOFT RESET')
|
|
117
|
-
self._conn.write(
|
|
127
|
+
self._conn.write(CTRL_D)
|
|
118
128
|
self._conn.read_until(b'soft reboot', timeout=1)
|
|
119
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
|
|
120
142
|
|
|
121
143
|
def exec(self, command, timeout=5):
|
|
122
144
|
"""Execute command
|
|
@@ -131,18 +153,17 @@ class MpyComm():
|
|
|
131
153
|
Raises:
|
|
132
154
|
CmdError when command return error
|
|
133
155
|
"""
|
|
134
|
-
# wait for prompt
|
|
135
156
|
self.enter_raw_repl()
|
|
136
157
|
if self._log:
|
|
137
158
|
self._log.info("CMD: %s", command)
|
|
138
159
|
self._conn.write(bytes(command, 'utf-8'))
|
|
139
|
-
self._conn.write(
|
|
160
|
+
self._conn.write(CTRL_D)
|
|
140
161
|
self._conn.read_until(b'OK', timeout)
|
|
141
|
-
result = self._conn.read_until(
|
|
162
|
+
result = self._conn.read_until(CTRL_D, timeout)
|
|
142
163
|
if result:
|
|
143
164
|
if self._log:
|
|
144
165
|
self._log.info('RES: %s', bytes(result))
|
|
145
|
-
err = self._conn.read_until(b'
|
|
166
|
+
err = self._conn.read_until(CTRL_D + b'>', timeout)
|
|
146
167
|
if err:
|
|
147
168
|
raise CmdError(command, result, err)
|
|
148
169
|
return result
|
|
@@ -150,3 +171,111 @@ class MpyComm():
|
|
|
150
171
|
def exec_eval(self, command, timeout=5):
|
|
151
172
|
result = self.exec(f'print({command})', timeout)
|
|
152
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
|