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/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(b'\x02') # Ctrl-B - exit raw REPL
83
+ self._conn.write(CTRL_B)
74
84
  else:
75
- self._conn.write(b'\x03') # Ctrl-C - interrupt program
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(b'\x01')
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(b'\x02')
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(b'\x04')
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(b'\x04')
160
+ self._conn.write(CTRL_D)
140
161
  self._conn.read_until(b'OK', timeout)
141
- result = self._conn.read_until(b'\x04', timeout)
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'\x04>', timeout)
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