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/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
- """Timeout"""
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.write(b'\x03')
55
- try:
56
- # wait for prompt
57
- self._conn.read_until(b'\r\n>>> ', timeout=1)
58
- except _conn.Timeout:
59
- # probably is in RAW repl
60
- if self._log:
61
- self._log.warning("Timeout while stopping program")
62
- self.exit_raw_repl()
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
- self.stop_current_operation()
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(b'\x01')
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(b'\x02')
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(b'\x04')
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(f"CMD: {command}")
158
+ self._log.info("CMD: %s", command)
109
159
  self._conn.write(bytes(command, 'utf-8'))
110
- self._conn.write(b'\x04')
160
+ self._conn.write(CTRL_D)
111
161
  self._conn.read_until(b'OK', timeout)
112
- result = self._conn.read_until(b'\x04', timeout)
162
+ result = self._conn.read_until(CTRL_D, timeout)
113
163
  if result:
114
164
  if self._log:
115
- self._log.info(f'RES: {result}')
116
- err = self._conn.read_until(b'\x04>', timeout)
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