replx-agent 2.0.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.
Potentially problematic release.
This version of replx-agent might be problematic. Click here for more details.
- replx_agent/__init__.py +3 -0
- replx_agent/__main__.py +5 -0
- replx_agent/commands.py +131 -0
- replx_agent/helpers/__init__.py +3 -0
- replx_agent/helpers/scanner.py +308 -0
- replx_agent/pkg/__init__.py +11 -0
- replx_agent/pkg/compiler.py +153 -0
- replx_agent/pkg/registry.py +387 -0
- replx_agent/pkg/service.py +690 -0
- replx_agent/pkg/store.py +83 -0
- replx_agent/protocol/__init__.py +11 -0
- replx_agent/protocol/repl.py +1029 -0
- replx_agent/protocol/storage.py +1018 -0
- replx_agent/rpc/__init__.py +3 -0
- replx_agent/rpc/protocol.py +220 -0
- replx_agent/server/__init__.py +3 -0
- replx_agent/server/__main__.py +4 -0
- replx_agent/server/command_dispatcher.py +49 -0
- replx_agent/server/connection_manager.py +654 -0
- replx_agent/server/core.py +1221 -0
- replx_agent/server/handlers/__init__.py +24 -0
- replx_agent/server/handlers/exec.py +553 -0
- replx_agent/server/handlers/filesystem.py +383 -0
- replx_agent/server/handlers/firmware.py +118 -0
- replx_agent/server/handlers/i2c.py +23 -0
- replx_agent/server/handlers/package.py +182 -0
- replx_agent/server/handlers/repl.py +188 -0
- replx_agent/server/handlers/session.py +205 -0
- replx_agent/server/handlers/spi.py +23 -0
- replx_agent/server/handlers/transfer.py +600 -0
- replx_agent/server/handlers/uart.py +32 -0
- replx_agent/server/session_manager.py +391 -0
- replx_agent/server/tcp.py +207 -0
- replx_agent/terminal.py +566 -0
- replx_agent/transport/__init__.py +32 -0
- replx_agent/transport/serial.py +220 -0
- replx_agent/utils/__init__.py +95 -0
- replx_agent/utils/constants.py +78 -0
- replx_agent/utils/device_info.py +183 -0
- replx_agent/utils/exceptions.py +31 -0
- replx_agent-2.0.0.dist-info/METADATA +74 -0
- replx_agent-2.0.0.dist-info/RECORD +46 -0
- replx_agent-2.0.0.dist-info/WHEEL +5 -0
- replx_agent-2.0.0.dist-info/entry_points.txt +2 -0
- replx_agent-2.0.0.dist-info/licenses/LICENSE +21 -0
- replx_agent-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1029 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import time
|
|
3
|
+
import struct
|
|
4
|
+
import textwrap
|
|
5
|
+
import threading
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
from typing import Callable, Optional
|
|
8
|
+
|
|
9
|
+
from replx_agent.utils.exceptions import ProtocolError, TransportError
|
|
10
|
+
from replx_agent.utils.constants import (
|
|
11
|
+
CTRL_A, CTRL_B, CTRL_C, CTRL_D, CTRL_E,
|
|
12
|
+
EOF_MARKER,
|
|
13
|
+
RAW_REPL_PROMPT, SOFT_REBOOT_MSG, OK_RESPONSE,
|
|
14
|
+
RAW_PASTE_INIT, RAW_PASTE_SUPPORTED, RAW_PASTE_NOT_SUPPORTED,
|
|
15
|
+
RAW_PASTE_WINDOW_INC, RAW_PASTE_END_DATA,
|
|
16
|
+
RAW_PASTE_DEFAULT_WINDOW_SIZE,
|
|
17
|
+
RAW_PASTE_SAFE_BAUD, RAW_PASTE_HIGH_BAUD_MIN_COMMAND_SIZE,
|
|
18
|
+
ERROR_HEADER,
|
|
19
|
+
)
|
|
20
|
+
from replx_agent.utils.device_info import get_core_profile, normalize_core
|
|
21
|
+
from replx_agent.transport import create_transport
|
|
22
|
+
from replx_agent.terminal import (
|
|
23
|
+
IS_WINDOWS, CR, LF,
|
|
24
|
+
flush_outbuf as _flush_outbuf,
|
|
25
|
+
stdout_write_bytes as _stdout_write_bytes,
|
|
26
|
+
getch, putch, getch_nonblock,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
_RAW_PASTE_MIN_COMMAND_SIZE = 4 * 1024
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ReplProtocol:
|
|
33
|
+
_active_instance = None
|
|
34
|
+
_sigint_handler_installed = False
|
|
35
|
+
|
|
36
|
+
def interrupt(self) -> None:
|
|
37
|
+
self.transport.write(CTRL_C)
|
|
38
|
+
|
|
39
|
+
def send_eof(self) -> None:
|
|
40
|
+
self.transport.write(CTRL_D)
|
|
41
|
+
|
|
42
|
+
def send_raw(self, data: bytes) -> None:
|
|
43
|
+
self.transport.write(data)
|
|
44
|
+
|
|
45
|
+
def drain(self) -> bytes:
|
|
46
|
+
return self.transport.read_available()
|
|
47
|
+
|
|
48
|
+
def enter_paste_mode(self) -> None:
|
|
49
|
+
self.transport.write(CTRL_E)
|
|
50
|
+
|
|
51
|
+
def exit_paste_mode(self) -> None:
|
|
52
|
+
self.transport.write(CTRL_B)
|
|
53
|
+
|
|
54
|
+
def in_waiting(self) -> int:
|
|
55
|
+
return self.transport.in_waiting()
|
|
56
|
+
|
|
57
|
+
def read_bytes(self, n: int) -> bytes:
|
|
58
|
+
return self.transport.read(n)
|
|
59
|
+
|
|
60
|
+
def __init__(self, connection_string, baudrate:int=115200, core:str="RP2350", device_root_fs:str="/"):
|
|
61
|
+
from replx_agent.transport import SerialTransport
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
if isinstance(connection_string, SerialTransport):
|
|
65
|
+
self.transport = connection_string
|
|
66
|
+
else:
|
|
67
|
+
self.transport = create_transport(connection_string, baudrate=baudrate)
|
|
68
|
+
self._stop_event = threading.Event()
|
|
69
|
+
self._follow_thread = None
|
|
70
|
+
except Exception as e:
|
|
71
|
+
raise ProtocolError(f"failed to open {connection_string} ({e})")
|
|
72
|
+
|
|
73
|
+
self._rx_pushback = bytearray()
|
|
74
|
+
self._raw_paste_supported = None
|
|
75
|
+
self._raw_paste_window_size = RAW_PASTE_DEFAULT_WINDOW_SIZE
|
|
76
|
+
|
|
77
|
+
self._skip_error_output = False
|
|
78
|
+
self._error_header_buf = b""
|
|
79
|
+
|
|
80
|
+
self.core = normalize_core(core)
|
|
81
|
+
self.device_root_fs = device_root_fs
|
|
82
|
+
|
|
83
|
+
_profile = get_core_profile(core)
|
|
84
|
+
self._DEVICE_CHUNK_SIZES = _profile['chunk_size']
|
|
85
|
+
self._RAW_MODE_DELAY = _profile['raw_mode_delay']
|
|
86
|
+
self._PUT_BATCH_BYTES = _profile['put_batch_bytes']
|
|
87
|
+
|
|
88
|
+
baud = getattr(self.transport, 'baudrate', None)
|
|
89
|
+
if not isinstance(baud, int) or baud <= 0:
|
|
90
|
+
baud = RAW_PASTE_SAFE_BAUD
|
|
91
|
+
self._raw_paste_min_command_size = (
|
|
92
|
+
_RAW_PASTE_MIN_COMMAND_SIZE if baud <= RAW_PASTE_SAFE_BAUD
|
|
93
|
+
else RAW_PASTE_HIGH_BAUD_MIN_COMMAND_SIZE
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
self._init_repl()
|
|
97
|
+
self._install_sigint_handler()
|
|
98
|
+
|
|
99
|
+
@classmethod
|
|
100
|
+
def _install_sigint_handler(cls):
|
|
101
|
+
if IS_WINDOWS and not cls._sigint_handler_installed:
|
|
102
|
+
import signal
|
|
103
|
+
|
|
104
|
+
def win_sigint_handler(signum, frame):
|
|
105
|
+
if cls._active_instance is None:
|
|
106
|
+
raise KeyboardInterrupt
|
|
107
|
+
cls._active_instance.request_interrupt()
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
signal.signal(signal.SIGINT, win_sigint_handler)
|
|
111
|
+
cls._sigint_handler_installed = True
|
|
112
|
+
except Exception:
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
def _reset_error_filter(self):
|
|
116
|
+
self._skip_error_output = False
|
|
117
|
+
self._error_header_buf = b""
|
|
118
|
+
|
|
119
|
+
def _init_repl(self):
|
|
120
|
+
self._interrupt_requested = False
|
|
121
|
+
self._session_depth = 0
|
|
122
|
+
self._in_raw_repl = False
|
|
123
|
+
self._repl_prompt_detected = False
|
|
124
|
+
|
|
125
|
+
def _read(self, n:int=1) -> bytes:
|
|
126
|
+
if self._rx_pushback:
|
|
127
|
+
if len(self._rx_pushback) <= n:
|
|
128
|
+
b = bytes(self._rx_pushback)
|
|
129
|
+
self._rx_pushback.clear()
|
|
130
|
+
rem = n - len(b)
|
|
131
|
+
if rem > 0:
|
|
132
|
+
b += self.transport.read(rem)
|
|
133
|
+
else:
|
|
134
|
+
b = bytes(self._rx_pushback[:n])
|
|
135
|
+
del self._rx_pushback[:n]
|
|
136
|
+
else:
|
|
137
|
+
b = self.transport.read(n)
|
|
138
|
+
|
|
139
|
+
return b
|
|
140
|
+
|
|
141
|
+
def _read_ex(self, min_num_bytes:int, ending:bytes, timeout:float=5.0,
|
|
142
|
+
data_consumer:Optional[Callable[[bytes], None]]=None) -> bytes:
|
|
143
|
+
start = time.monotonic()
|
|
144
|
+
deadline = (start + timeout) if timeout > 0 else None
|
|
145
|
+
|
|
146
|
+
streaming_mode = (timeout == 0 and data_consumer is not None)
|
|
147
|
+
|
|
148
|
+
pat = ending
|
|
149
|
+
m = len(pat)
|
|
150
|
+
pi = [0] * m
|
|
151
|
+
k = 0
|
|
152
|
+
for i in range(1, m):
|
|
153
|
+
while k > 0 and pat[k] != pat[i]:
|
|
154
|
+
k = pi[k-1]
|
|
155
|
+
if pat[k] == pat[i]:
|
|
156
|
+
k += 1
|
|
157
|
+
pi[i] = k
|
|
158
|
+
|
|
159
|
+
matched = 0
|
|
160
|
+
crash_tail = b''
|
|
161
|
+
|
|
162
|
+
tail_size = m + 8
|
|
163
|
+
tail_buffer = bytearray(tail_size)
|
|
164
|
+
tail_len = 0
|
|
165
|
+
data = bytearray()
|
|
166
|
+
|
|
167
|
+
def _call_consumer(chunk: bytes):
|
|
168
|
+
if data_consumer and chunk:
|
|
169
|
+
data_consumer(chunk)
|
|
170
|
+
|
|
171
|
+
def _update_tail(chunk: bytes):
|
|
172
|
+
nonlocal tail_len
|
|
173
|
+
chunk_len = len(chunk)
|
|
174
|
+
if chunk_len >= tail_size:
|
|
175
|
+
tail_buffer[:] = chunk[-tail_size:]
|
|
176
|
+
tail_len = tail_size
|
|
177
|
+
elif tail_len + chunk_len <= tail_size:
|
|
178
|
+
tail_buffer[tail_len:tail_len + chunk_len] = chunk
|
|
179
|
+
tail_len += chunk_len
|
|
180
|
+
else:
|
|
181
|
+
keep = tail_size - chunk_len
|
|
182
|
+
tail_buffer[:keep] = tail_buffer[tail_len - keep:tail_len]
|
|
183
|
+
tail_buffer[keep:keep + chunk_len] = chunk
|
|
184
|
+
tail_len = tail_size
|
|
185
|
+
|
|
186
|
+
def _feed_chunk(chunk: bytes) -> int:
|
|
187
|
+
nonlocal matched, crash_tail
|
|
188
|
+
|
|
189
|
+
if not chunk:
|
|
190
|
+
return -1
|
|
191
|
+
if ending == EOF_MARKER and self.core.startswith('ESP32'):
|
|
192
|
+
crash_tail = (crash_tail + chunk)[-8192:]
|
|
193
|
+
if (b'\r\nGuru Meditation Error:' in crash_tail
|
|
194
|
+
or b'\r\nA fatal error occurred. The crash dump printed below' in crash_tail):
|
|
195
|
+
raise ProtocolError(
|
|
196
|
+
"ESP32 firmware crashed (native panic/core dump), not a REPL timeout. "
|
|
197
|
+
"Power-cycle the board. On P4/C6 check the MicroPython C6_WIFI build, "
|
|
198
|
+
"chip revision and ESP-Hosted coprocessor firmware; Python retries cannot fix this."
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
clen = len(chunk)
|
|
202
|
+
end_at = -1
|
|
203
|
+
|
|
204
|
+
if matched == 0:
|
|
205
|
+
pos = chunk.find(pat)
|
|
206
|
+
if pos >= 0:
|
|
207
|
+
end_at = pos + m - 1
|
|
208
|
+
matched = m
|
|
209
|
+
else:
|
|
210
|
+
tail_start = max(0, clen - m + 1)
|
|
211
|
+
for idx in range(tail_start, clen):
|
|
212
|
+
b = chunk[idx]
|
|
213
|
+
while matched > 0 and pat[matched] != b:
|
|
214
|
+
matched = pi[matched - 1]
|
|
215
|
+
if pat[matched] == b:
|
|
216
|
+
matched += 1
|
|
217
|
+
if matched == m:
|
|
218
|
+
end_at = idx
|
|
219
|
+
break
|
|
220
|
+
else:
|
|
221
|
+
remaining = m - matched
|
|
222
|
+
if clen >= remaining and chunk[:remaining] == pat[matched:]:
|
|
223
|
+
end_at = remaining - 1
|
|
224
|
+
matched = m
|
|
225
|
+
else:
|
|
226
|
+
for idx in range(clen):
|
|
227
|
+
b = chunk[idx]
|
|
228
|
+
while matched > 0 and pat[matched] != b:
|
|
229
|
+
matched = pi[matched - 1]
|
|
230
|
+
if pat[matched] == b:
|
|
231
|
+
matched += 1
|
|
232
|
+
if matched == m:
|
|
233
|
+
end_at = idx
|
|
234
|
+
break
|
|
235
|
+
|
|
236
|
+
if streaming_mode:
|
|
237
|
+
chunk_to_process = chunk[:end_at+1] if end_at >= 0 else chunk
|
|
238
|
+
_call_consumer(chunk_to_process)
|
|
239
|
+
_update_tail(chunk_to_process)
|
|
240
|
+
return end_at + 1 if end_at >= 0 else -1
|
|
241
|
+
else:
|
|
242
|
+
if end_at >= 0:
|
|
243
|
+
data.extend(chunk[:end_at+1])
|
|
244
|
+
_call_consumer(chunk[:end_at+1])
|
|
245
|
+
return end_at + 1
|
|
246
|
+
|
|
247
|
+
data.extend(chunk)
|
|
248
|
+
_call_consumer(chunk)
|
|
249
|
+
return -1
|
|
250
|
+
|
|
251
|
+
def _read_some(max_n: int) -> bytes:
|
|
252
|
+
if self._rx_pushback:
|
|
253
|
+
# Return buffered bytes without blocking for the rest of max_n;
|
|
254
|
+
# self._read() would wait a full serial timeout for them.
|
|
255
|
+
b = bytes(self._rx_pushback[:max(1, max_n)])
|
|
256
|
+
del self._rx_pushback[:len(b)]
|
|
257
|
+
return b
|
|
258
|
+
try:
|
|
259
|
+
avail = self.transport.read_available()
|
|
260
|
+
if avail:
|
|
261
|
+
return avail
|
|
262
|
+
except TransportError:
|
|
263
|
+
raise
|
|
264
|
+
|
|
265
|
+
if streaming_mode:
|
|
266
|
+
return b''
|
|
267
|
+
return self._read(1)
|
|
268
|
+
|
|
269
|
+
def _get_result() -> bytes:
|
|
270
|
+
if streaming_mode:
|
|
271
|
+
return bytes(tail_buffer[:tail_len])
|
|
272
|
+
else:
|
|
273
|
+
return bytes(data)
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
if min_num_bytes > 0:
|
|
277
|
+
chunk = _read_some(min_num_bytes)
|
|
278
|
+
if chunk:
|
|
279
|
+
pos = _feed_chunk(chunk)
|
|
280
|
+
if pos >= 0:
|
|
281
|
+
tail = chunk[pos:]
|
|
282
|
+
if tail:
|
|
283
|
+
self._rx_pushback[:] = tail + self._rx_pushback
|
|
284
|
+
return _get_result()
|
|
285
|
+
|
|
286
|
+
last_activity = time.monotonic()
|
|
287
|
+
last_keepalive = time.monotonic()
|
|
288
|
+
keepalive_interval = 30.0
|
|
289
|
+
|
|
290
|
+
while True:
|
|
291
|
+
if self._stop_event.is_set():
|
|
292
|
+
break
|
|
293
|
+
|
|
294
|
+
if deadline is not None and time.monotonic() >= deadline:
|
|
295
|
+
break
|
|
296
|
+
|
|
297
|
+
now = time.monotonic()
|
|
298
|
+
if streaming_mode and (now - last_keepalive) >= keepalive_interval:
|
|
299
|
+
if hasattr(self.transport, 'keep_alive'):
|
|
300
|
+
self.transport.keep_alive()
|
|
301
|
+
last_keepalive = now
|
|
302
|
+
|
|
303
|
+
waiting = self.transport.in_waiting()
|
|
304
|
+
if waiting <= 0 and len(self._rx_pushback) > 0:
|
|
305
|
+
waiting = 1
|
|
306
|
+
|
|
307
|
+
if waiting > 0:
|
|
308
|
+
want = min(4096, max(256, waiting))
|
|
309
|
+
chunk = _read_some(want)
|
|
310
|
+
if chunk:
|
|
311
|
+
pos = _feed_chunk(chunk)
|
|
312
|
+
last_activity = time.monotonic()
|
|
313
|
+
if pos >= 0:
|
|
314
|
+
tail = chunk[pos:]
|
|
315
|
+
if tail:
|
|
316
|
+
self._rx_pushback[:] = tail + self._rx_pushback
|
|
317
|
+
return _get_result()
|
|
318
|
+
else:
|
|
319
|
+
time.sleep(0.001)
|
|
320
|
+
else:
|
|
321
|
+
time.sleep(0.001)
|
|
322
|
+
|
|
323
|
+
if timeout > 0:
|
|
324
|
+
idle_limit = max(timeout * 2, 10)
|
|
325
|
+
if (time.monotonic() - last_activity) > idle_limit:
|
|
326
|
+
break
|
|
327
|
+
|
|
328
|
+
return _get_result()
|
|
329
|
+
|
|
330
|
+
except TransportError:
|
|
331
|
+
if not self._stop_event.is_set():
|
|
332
|
+
raise
|
|
333
|
+
return _get_result()
|
|
334
|
+
except ProtocolError:
|
|
335
|
+
raise
|
|
336
|
+
except Exception as exc:
|
|
337
|
+
raise ProtocolError(f"REPL read failed while waiting for {ending!r}: {exc}") from exc
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _enter_repl(self, timeout: float = 5.0, attempts: int = 4):
|
|
341
|
+
self._in_raw_repl = False
|
|
342
|
+
self._rx_pushback.clear()
|
|
343
|
+
delay = 0.1
|
|
344
|
+
|
|
345
|
+
for attempt in range(1, attempts + 1):
|
|
346
|
+
try:
|
|
347
|
+
self.transport.reset_input_buffer()
|
|
348
|
+
time.sleep(0.05)
|
|
349
|
+
self.transport.reset_output_buffer()
|
|
350
|
+
time.sleep(0.05)
|
|
351
|
+
except Exception:
|
|
352
|
+
pass
|
|
353
|
+
|
|
354
|
+
self.transport.write(b'\r' + CTRL_C + CTRL_C)
|
|
355
|
+
time.sleep(delay)
|
|
356
|
+
|
|
357
|
+
try:
|
|
358
|
+
self.transport.reset_input_buffer()
|
|
359
|
+
time.sleep(0.05)
|
|
360
|
+
except Exception:
|
|
361
|
+
pass
|
|
362
|
+
|
|
363
|
+
self.transport.write(b'\r' + CTRL_A)
|
|
364
|
+
|
|
365
|
+
try:
|
|
366
|
+
data = self._read_ex(1, RAW_REPL_PROMPT[:-1], timeout=timeout)
|
|
367
|
+
if not data.endswith(RAW_REPL_PROMPT[:-1]):
|
|
368
|
+
raise ProtocolError('could not enter raw repl')
|
|
369
|
+
self._in_raw_repl = True
|
|
370
|
+
return
|
|
371
|
+
except ProtocolError:
|
|
372
|
+
try:
|
|
373
|
+
self.transport.write(b'\r' + CTRL_B)
|
|
374
|
+
time.sleep(0.15)
|
|
375
|
+
try:
|
|
376
|
+
self.transport.reset_input_buffer()
|
|
377
|
+
except Exception:
|
|
378
|
+
pass
|
|
379
|
+
except Exception:
|
|
380
|
+
pass
|
|
381
|
+
|
|
382
|
+
time.sleep(0.2 * attempt)
|
|
383
|
+
continue
|
|
384
|
+
|
|
385
|
+
raise ProtocolError('could not enter raw repl')
|
|
386
|
+
|
|
387
|
+
def probe(self, timeout: float = 1.0) -> bool:
|
|
388
|
+
if self._session_depth:
|
|
389
|
+
raise ProtocolError("Cannot probe the REPL during an active session")
|
|
390
|
+
if self._stop_event.is_set():
|
|
391
|
+
raise TransportError("REPL is stopping")
|
|
392
|
+
if self._rx_pushback or self.transport.in_waiting() > 0:
|
|
393
|
+
return True
|
|
394
|
+
|
|
395
|
+
try:
|
|
396
|
+
self._enter_repl(timeout=timeout, attempts=1)
|
|
397
|
+
self._leave_repl()
|
|
398
|
+
return True
|
|
399
|
+
except (ProtocolError, TransportError, OSError):
|
|
400
|
+
self._in_raw_repl = False
|
|
401
|
+
self._rx_pushback.clear()
|
|
402
|
+
raise
|
|
403
|
+
|
|
404
|
+
def _soft_reset(self):
|
|
405
|
+
timeout = 3
|
|
406
|
+
try:
|
|
407
|
+
self.transport.write(CTRL_C)
|
|
408
|
+
time.sleep(0.1)
|
|
409
|
+
self.transport.write(CTRL_C)
|
|
410
|
+
time.sleep(0.1)
|
|
411
|
+
self.transport.reset_input_buffer()
|
|
412
|
+
|
|
413
|
+
self.transport.write(CTRL_A)
|
|
414
|
+
time.sleep(0.1)
|
|
415
|
+
self.transport.reset_input_buffer()
|
|
416
|
+
|
|
417
|
+
self.transport.write(CTRL_D)
|
|
418
|
+
data = self._read_ex(1, SOFT_REBOOT_MSG, timeout=timeout)
|
|
419
|
+
if not data.endswith(SOFT_REBOOT_MSG):
|
|
420
|
+
raise ProtocolError('soft reset failed')
|
|
421
|
+
|
|
422
|
+
data = self._read_ex(1, RAW_REPL_PROMPT[:-1], timeout=timeout)
|
|
423
|
+
if not data.endswith(RAW_REPL_PROMPT[:-1]):
|
|
424
|
+
raise ProtocolError('soft reset failed')
|
|
425
|
+
except Exception:
|
|
426
|
+
raise
|
|
427
|
+
|
|
428
|
+
def _resync_repl(self):
|
|
429
|
+
try:
|
|
430
|
+
self.transport.write(b'\r' + CTRL_B)
|
|
431
|
+
time.sleep(0.04)
|
|
432
|
+
self.transport.reset_input_buffer()
|
|
433
|
+
except Exception:
|
|
434
|
+
pass
|
|
435
|
+
|
|
436
|
+
try:
|
|
437
|
+
self.transport.write(b'\r' + CTRL_A)
|
|
438
|
+
got = self._read_ex(1, RAW_REPL_PROMPT[:-1], timeout=1)
|
|
439
|
+
if got.endswith(RAW_REPL_PROMPT[:-1]):
|
|
440
|
+
return True
|
|
441
|
+
except Exception:
|
|
442
|
+
pass
|
|
443
|
+
return False
|
|
444
|
+
|
|
445
|
+
def _leave_repl(self):
|
|
446
|
+
self.transport.write(b'\r' + CTRL_B)
|
|
447
|
+
self._in_raw_repl = False
|
|
448
|
+
|
|
449
|
+
@contextmanager
|
|
450
|
+
def session(self):
|
|
451
|
+
need_enter = self._session_depth == 0
|
|
452
|
+
if need_enter:
|
|
453
|
+
self._enter_repl()
|
|
454
|
+
self._raw_paste_supported = None
|
|
455
|
+
self._session_depth += 1
|
|
456
|
+
try:
|
|
457
|
+
yield
|
|
458
|
+
finally:
|
|
459
|
+
self._session_depth = max(0, self._session_depth - 1)
|
|
460
|
+
if need_enter:
|
|
461
|
+
self._leave_repl()
|
|
462
|
+
|
|
463
|
+
def _enter_raw_paste_mode(self) -> bool:
|
|
464
|
+
if not get_core_profile(self.core)['raw_paste']:
|
|
465
|
+
return False
|
|
466
|
+
# Only a negative result may be cached: every raw-paste exec must
|
|
467
|
+
# start with its own RAW_PASTE_INIT handshake.
|
|
468
|
+
if self._raw_paste_supported is False:
|
|
469
|
+
return False
|
|
470
|
+
|
|
471
|
+
self.transport.write(RAW_PASTE_INIT)
|
|
472
|
+
|
|
473
|
+
start = time.monotonic()
|
|
474
|
+
timeout = 0.5
|
|
475
|
+
response = b''
|
|
476
|
+
while len(response) < 2 and (time.monotonic() - start) < timeout:
|
|
477
|
+
waiting = self.transport.in_waiting()
|
|
478
|
+
if waiting > 0:
|
|
479
|
+
response += self._read(min(2 - len(response), waiting))
|
|
480
|
+
else:
|
|
481
|
+
time.sleep(0.01)
|
|
482
|
+
|
|
483
|
+
if len(response) < 2:
|
|
484
|
+
self._raw_paste_supported = False
|
|
485
|
+
if not self._resync_repl():
|
|
486
|
+
raise ProtocolError("Timed out negotiating raw-paste mode and could not restore the REPL")
|
|
487
|
+
raise ProtocolError("Timed out negotiating raw-paste mode")
|
|
488
|
+
|
|
489
|
+
if response == RAW_PASTE_SUPPORTED:
|
|
490
|
+
window_bytes = self._read(2)
|
|
491
|
+
if len(window_bytes) != 2:
|
|
492
|
+
self.transport.write(CTRL_D)
|
|
493
|
+
self._read_ex(1, b'>', timeout=1)
|
|
494
|
+
self._raw_paste_supported = False
|
|
495
|
+
raise ProtocolError("Incomplete raw-paste window-size response")
|
|
496
|
+
|
|
497
|
+
window_size = struct.unpack('<H', window_bytes)[0]
|
|
498
|
+
if window_size == 0:
|
|
499
|
+
self.transport.write(CTRL_D)
|
|
500
|
+
self._read_ex(1, b'>', timeout=1)
|
|
501
|
+
self._raw_paste_supported = False
|
|
502
|
+
raise ProtocolError("Invalid raw-paste window size: 0")
|
|
503
|
+
|
|
504
|
+
self._raw_paste_window_size = window_size
|
|
505
|
+
self._raw_paste_supported = True
|
|
506
|
+
return True
|
|
507
|
+
|
|
508
|
+
if response == RAW_PASTE_NOT_SUPPORTED:
|
|
509
|
+
self._raw_paste_supported = False
|
|
510
|
+
return False
|
|
511
|
+
|
|
512
|
+
if response == b'ra':
|
|
513
|
+
self._read_ex(1, b'>', timeout=1)
|
|
514
|
+
self._raw_paste_supported = False
|
|
515
|
+
return False
|
|
516
|
+
|
|
517
|
+
self._raw_paste_supported = False
|
|
518
|
+
if not self._resync_repl():
|
|
519
|
+
raise ProtocolError(f"Unexpected raw-paste response {response!r}; could not restore the REPL")
|
|
520
|
+
raise ProtocolError(f"Unexpected raw-paste response {response!r}")
|
|
521
|
+
|
|
522
|
+
def _follow_task(self, echo: bool):
|
|
523
|
+
try:
|
|
524
|
+
while not self._stop_event.is_set():
|
|
525
|
+
try:
|
|
526
|
+
ch = getch_nonblock()
|
|
527
|
+
if ch is None:
|
|
528
|
+
time.sleep(0.005)
|
|
529
|
+
continue
|
|
530
|
+
|
|
531
|
+
if not ch:
|
|
532
|
+
continue
|
|
533
|
+
|
|
534
|
+
if ch == CTRL_C:
|
|
535
|
+
self.request_interrupt()
|
|
536
|
+
time.sleep(0.1)
|
|
537
|
+
return
|
|
538
|
+
|
|
539
|
+
if ch == CTRL_D:
|
|
540
|
+
try:
|
|
541
|
+
self.transport.write(CTRL_D)
|
|
542
|
+
except Exception:
|
|
543
|
+
pass
|
|
544
|
+
time.sleep(0.08)
|
|
545
|
+
return
|
|
546
|
+
|
|
547
|
+
if echo:
|
|
548
|
+
putch(ch)
|
|
549
|
+
|
|
550
|
+
self.transport.write(CR if ch == LF else ch)
|
|
551
|
+
except Exception:
|
|
552
|
+
pass
|
|
553
|
+
finally:
|
|
554
|
+
pass
|
|
555
|
+
|
|
556
|
+
def _exec_raw_paste(self, command: bytes, data_consumer: Optional[Callable[[bytes], None]] = None) -> tuple[bytes, bytes]:
|
|
557
|
+
remaining_window = self._raw_paste_window_size
|
|
558
|
+
bytes_sent = 0
|
|
559
|
+
command_len = len(command)
|
|
560
|
+
aborted = False
|
|
561
|
+
|
|
562
|
+
while bytes_sent < command_len:
|
|
563
|
+
while remaining_window <= 0 or self.transport.in_waiting() > 0:
|
|
564
|
+
fc_byte = self._read(1)
|
|
565
|
+
if fc_byte == RAW_PASTE_WINDOW_INC:
|
|
566
|
+
remaining_window += self._raw_paste_window_size
|
|
567
|
+
elif fc_byte == RAW_PASTE_END_DATA:
|
|
568
|
+
self.transport.write(CTRL_D)
|
|
569
|
+
aborted = True
|
|
570
|
+
break
|
|
571
|
+
elif fc_byte:
|
|
572
|
+
raise ProtocolError(
|
|
573
|
+
f"Unexpected raw-paste flow-control byte: {fc_byte!r}"
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
if aborted:
|
|
577
|
+
break
|
|
578
|
+
|
|
579
|
+
chunk_size = min(remaining_window, command_len - bytes_sent)
|
|
580
|
+
chunk = command[bytes_sent:bytes_sent + chunk_size]
|
|
581
|
+
self.transport.write(chunk)
|
|
582
|
+
bytes_sent += chunk_size
|
|
583
|
+
remaining_window -= chunk_size
|
|
584
|
+
|
|
585
|
+
if not aborted and bytes_sent == command_len:
|
|
586
|
+
self.transport.write(CTRL_D)
|
|
587
|
+
|
|
588
|
+
ack = self._read_ex(1, EOF_MARKER, timeout=5)
|
|
589
|
+
if not ack.endswith(EOF_MARKER):
|
|
590
|
+
raise ProtocolError("Raw-paste compilation acknowledgment timeout")
|
|
591
|
+
|
|
592
|
+
stdout_data = self._read_ex(1, EOF_MARKER, timeout=0, data_consumer=data_consumer)
|
|
593
|
+
if stdout_data.endswith(EOF_MARKER):
|
|
594
|
+
stdout_data = stdout_data[:-1]
|
|
595
|
+
|
|
596
|
+
stderr_data = self._read_ex(1, EOF_MARKER, timeout=5)
|
|
597
|
+
if stderr_data.endswith(EOF_MARKER):
|
|
598
|
+
stderr_data = stderr_data[:-1]
|
|
599
|
+
|
|
600
|
+
# Leave the trailing raw-REPL '>' for the next command's prompt check.
|
|
601
|
+
return (stdout_data, stderr_data)
|
|
602
|
+
|
|
603
|
+
def _exec(self, command:str|bytes, interactive:bool=False, echo:bool=False, detach:bool=False,
|
|
604
|
+
data_consumer:Optional[Callable[[bytes], None]]=None,
|
|
605
|
+
force_raw_paste:Optional[bool]=None) -> bytes:
|
|
606
|
+
self._stop_event.clear()
|
|
607
|
+
self._reset_error_filter()
|
|
608
|
+
|
|
609
|
+
if isinstance(command, str):
|
|
610
|
+
command = command.encode('utf-8')
|
|
611
|
+
|
|
612
|
+
if not command or len(command) == 0:
|
|
613
|
+
return b''
|
|
614
|
+
|
|
615
|
+
if interactive:
|
|
616
|
+
ReplProtocol._active_instance = self
|
|
617
|
+
|
|
618
|
+
try:
|
|
619
|
+
return self._exec_command(command, interactive, echo, detach, data_consumer, force_raw_paste)
|
|
620
|
+
except ProtocolError:
|
|
621
|
+
self._in_raw_repl = False
|
|
622
|
+
self._rx_pushback.clear()
|
|
623
|
+
raise
|
|
624
|
+
finally:
|
|
625
|
+
if interactive:
|
|
626
|
+
ReplProtocol._active_instance = None
|
|
627
|
+
|
|
628
|
+
def _exec_command(self, command: bytes, interactive: bool, echo: bool, detach: bool,
|
|
629
|
+
data_consumer: Optional[Callable[[bytes], None]],
|
|
630
|
+
force_raw_paste: Optional[bool]) -> bytes:
|
|
631
|
+
data_err = b''
|
|
632
|
+
if data_consumer is None and interactive:
|
|
633
|
+
data_consumer = self._create_data_consumer()
|
|
634
|
+
follow_thread = None
|
|
635
|
+
|
|
636
|
+
data = self._read_ex(1, b'>')
|
|
637
|
+
if not data.endswith(b'>'):
|
|
638
|
+
raise ProtocolError('could not enter raw repl')
|
|
639
|
+
|
|
640
|
+
command_len = len(command)
|
|
641
|
+
|
|
642
|
+
prefer_raw_paste = force_raw_paste
|
|
643
|
+
if prefer_raw_paste is None:
|
|
644
|
+
prefer_raw_paste = command_len >= self._raw_paste_min_command_size
|
|
645
|
+
use_raw_paste = (
|
|
646
|
+
prefer_raw_paste
|
|
647
|
+
and not interactive
|
|
648
|
+
and not detach
|
|
649
|
+
and self._enter_raw_paste_mode()
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
if use_raw_paste:
|
|
653
|
+
try:
|
|
654
|
+
data, data_err = self._exec_raw_paste(command, data_consumer)
|
|
655
|
+
except TransportError:
|
|
656
|
+
raise
|
|
657
|
+
except Exception as e:
|
|
658
|
+
self._raw_paste_supported = False
|
|
659
|
+
if not self._resync_repl():
|
|
660
|
+
try:
|
|
661
|
+
self._leave_repl()
|
|
662
|
+
time.sleep(0.05)
|
|
663
|
+
self._enter_repl()
|
|
664
|
+
prompt = self._read_ex(1, b'>')
|
|
665
|
+
if not prompt.endswith(b'>'):
|
|
666
|
+
raise ProtocolError('could not recover after raw-paste failure')
|
|
667
|
+
except Exception as recovery_error:
|
|
668
|
+
raise ProtocolError(
|
|
669
|
+
f"Raw-paste transfer failed: {e}; REPL recovery failed: {recovery_error}"
|
|
670
|
+
) from e
|
|
671
|
+
if isinstance(e, ProtocolError):
|
|
672
|
+
raise
|
|
673
|
+
raise ProtocolError(f"Raw-paste transfer failed: {e}") from e
|
|
674
|
+
|
|
675
|
+
if data_err and not self._interrupt_requested:
|
|
676
|
+
raise ProtocolError(data_err.decode('utf-8', errors='replace'))
|
|
677
|
+
|
|
678
|
+
self._interrupt_requested = False
|
|
679
|
+
return data
|
|
680
|
+
|
|
681
|
+
current_buffer_size = self._DEVICE_CHUNK_SIZES
|
|
682
|
+
bytes_sent = 0
|
|
683
|
+
|
|
684
|
+
start_time = time.monotonic()
|
|
685
|
+
|
|
686
|
+
while bytes_sent < command_len:
|
|
687
|
+
chunk_end = min(bytes_sent + current_buffer_size, command_len)
|
|
688
|
+
chunk = command[bytes_sent:chunk_end]
|
|
689
|
+
|
|
690
|
+
self.transport.write(chunk)
|
|
691
|
+
bytes_sent += len(chunk)
|
|
692
|
+
|
|
693
|
+
if self._RAW_MODE_DELAY > 0 and bytes_sent < command_len:
|
|
694
|
+
time.sleep(self._RAW_MODE_DELAY)
|
|
695
|
+
|
|
696
|
+
self.transport.write(EOF_MARKER)
|
|
697
|
+
|
|
698
|
+
transfer_time = time.monotonic() - start_time
|
|
699
|
+
timeout = max(5, int(transfer_time * 2))
|
|
700
|
+
|
|
701
|
+
data = self._read_ex(1, OK_RESPONSE, timeout=timeout)
|
|
702
|
+
if not data.endswith(OK_RESPONSE):
|
|
703
|
+
raise ProtocolError('could not execute command (response: %r)' % data)
|
|
704
|
+
|
|
705
|
+
if detach:
|
|
706
|
+
return b''
|
|
707
|
+
|
|
708
|
+
if interactive:
|
|
709
|
+
self._stop_event.clear()
|
|
710
|
+
self._interrupt_requested = False
|
|
711
|
+
follow_thread = threading.Thread(target=self._follow_task, args=(echo,), daemon=True)
|
|
712
|
+
follow_thread.start()
|
|
713
|
+
|
|
714
|
+
try:
|
|
715
|
+
data = self._read_ex(1, EOF_MARKER, 0, data_consumer)
|
|
716
|
+
if data.endswith(EOF_MARKER):
|
|
717
|
+
data = data[:-1]
|
|
718
|
+
|
|
719
|
+
data_err = self._read_ex(1, EOF_MARKER, 0, None)
|
|
720
|
+
if data_err.endswith(EOF_MARKER):
|
|
721
|
+
data_err = data_err[:-1]
|
|
722
|
+
elif data_err and not self._interrupt_requested:
|
|
723
|
+
raise ProtocolError(data_err.decode('utf-8', errors='replace'))
|
|
724
|
+
finally:
|
|
725
|
+
def _drain_consumer(b: bytes, _carry=[False]):
|
|
726
|
+
if len(b) == 1 and b == b'>':
|
|
727
|
+
return
|
|
728
|
+
if data_consumer:
|
|
729
|
+
data_consumer(b)
|
|
730
|
+
else:
|
|
731
|
+
_stdout_write_bytes(b)
|
|
732
|
+
|
|
733
|
+
if follow_thread and follow_thread.is_alive():
|
|
734
|
+
self._stop_event.set()
|
|
735
|
+
try:
|
|
736
|
+
follow_thread.join(timeout=0.05)
|
|
737
|
+
except Exception:
|
|
738
|
+
pass
|
|
739
|
+
|
|
740
|
+
self._stop_event.clear()
|
|
741
|
+
|
|
742
|
+
try:
|
|
743
|
+
self._read_ex(1, EOF_MARKER, timeout=0.1, data_consumer=_drain_consumer)
|
|
744
|
+
except Exception:
|
|
745
|
+
pass
|
|
746
|
+
|
|
747
|
+
if not data_consumer:
|
|
748
|
+
try:
|
|
749
|
+
_flush_outbuf()
|
|
750
|
+
except Exception:
|
|
751
|
+
pass
|
|
752
|
+
|
|
753
|
+
try:
|
|
754
|
+
if self._interrupt_requested and self.core != "EFR32MG":
|
|
755
|
+
self.transport.write(b'\r' + CTRL_B)
|
|
756
|
+
time.sleep(0.08)
|
|
757
|
+
self.transport.write(b'\r' + CTRL_A)
|
|
758
|
+
self._read_ex(1, RAW_REPL_PROMPT[:-1], timeout=1)
|
|
759
|
+
else:
|
|
760
|
+
self.transport.write(CTRL_D)
|
|
761
|
+
got = self._read_ex(1, SOFT_REBOOT_MSG, timeout=2)
|
|
762
|
+
if not got.endswith(SOFT_REBOOT_MSG):
|
|
763
|
+
self.transport.write(b'\r' + CTRL_B)
|
|
764
|
+
time.sleep(0.08)
|
|
765
|
+
self.transport.write(b'\r' + CTRL_A)
|
|
766
|
+
self._read_ex(1, RAW_REPL_PROMPT[:-1], timeout=1)
|
|
767
|
+
except Exception:
|
|
768
|
+
pass
|
|
769
|
+
|
|
770
|
+
if data_err:
|
|
771
|
+
if self._interrupt_requested:
|
|
772
|
+
data_err = b""
|
|
773
|
+
else:
|
|
774
|
+
raise ProtocolError(data_err.decode('utf-8', errors='replace'))
|
|
775
|
+
|
|
776
|
+
self._interrupt_requested = False
|
|
777
|
+
return data
|
|
778
|
+
|
|
779
|
+
def _create_data_consumer(self):
|
|
780
|
+
def data_consumer(chunk):
|
|
781
|
+
if not chunk:
|
|
782
|
+
return
|
|
783
|
+
|
|
784
|
+
self._error_header_buf = (self._error_header_buf + chunk)[-len(ERROR_HEADER):]
|
|
785
|
+
if self._error_header_buf == ERROR_HEADER:
|
|
786
|
+
self._skip_error_output = True
|
|
787
|
+
return
|
|
788
|
+
|
|
789
|
+
if not self._skip_error_output:
|
|
790
|
+
_stdout_write_bytes(chunk)
|
|
791
|
+
_flush_outbuf()
|
|
792
|
+
|
|
793
|
+
return data_consumer
|
|
794
|
+
|
|
795
|
+
def _drain_eof(self, max_ms:int=200):
|
|
796
|
+
deadline = time.monotonic() + max_ms / 1000
|
|
797
|
+
while time.monotonic() < deadline:
|
|
798
|
+
waiting = self.transport.in_waiting()
|
|
799
|
+
if waiting:
|
|
800
|
+
_ = self._read(waiting)
|
|
801
|
+
else:
|
|
802
|
+
time.sleep(0.01)
|
|
803
|
+
|
|
804
|
+
def _repl_serial_to_stdout(self):
|
|
805
|
+
PROMPT_COLOR = b"\033[92m"
|
|
806
|
+
CONT_COLOR = b"\033[93m"
|
|
807
|
+
RESET_COLOR = b"\033[0m"
|
|
808
|
+
read_errors = 0
|
|
809
|
+
|
|
810
|
+
try:
|
|
811
|
+
while self.serial_reader_running:
|
|
812
|
+
try:
|
|
813
|
+
count = self.transport.in_waiting()
|
|
814
|
+
except Exception:
|
|
815
|
+
break
|
|
816
|
+
|
|
817
|
+
if not count:
|
|
818
|
+
read_errors = 0
|
|
819
|
+
time.sleep(0.01)
|
|
820
|
+
continue
|
|
821
|
+
|
|
822
|
+
try:
|
|
823
|
+
data = self.transport.read(count)
|
|
824
|
+
except Exception:
|
|
825
|
+
break
|
|
826
|
+
|
|
827
|
+
if not data:
|
|
828
|
+
continue
|
|
829
|
+
|
|
830
|
+
read_errors = 0
|
|
831
|
+
|
|
832
|
+
if b">>> " in data:
|
|
833
|
+
self._repl_prompt_detected = True
|
|
834
|
+
|
|
835
|
+
if self.serial_out_put_enable and self.serial_out_put_count > 0:
|
|
836
|
+
data = data.replace(b">>> ", PROMPT_COLOR + b">>> " + RESET_COLOR)
|
|
837
|
+
data = data.replace(b"... ", CONT_COLOR + b"... " + RESET_COLOR)
|
|
838
|
+
|
|
839
|
+
if IS_WINDOWS:
|
|
840
|
+
sys.stdout.buffer.write(data.replace(b"\r", b""))
|
|
841
|
+
else:
|
|
842
|
+
sys.stdout.buffer.write(data)
|
|
843
|
+
sys.stdout.buffer.flush()
|
|
844
|
+
|
|
845
|
+
self.serial_out_put_count += 1
|
|
846
|
+
except KeyboardInterrupt:
|
|
847
|
+
try:
|
|
848
|
+
self.transport.close()
|
|
849
|
+
except Exception:
|
|
850
|
+
pass
|
|
851
|
+
|
|
852
|
+
def _reset(self):
|
|
853
|
+
command = """
|
|
854
|
+
import machine
|
|
855
|
+
machine.soft_reset() # Ctrl+D
|
|
856
|
+
"""
|
|
857
|
+
self.exec(command)
|
|
858
|
+
|
|
859
|
+
def request_interrupt(self):
|
|
860
|
+
self._interrupt_requested = True
|
|
861
|
+
try:
|
|
862
|
+
self.transport.write(CTRL_C)
|
|
863
|
+
except Exception:
|
|
864
|
+
pass
|
|
865
|
+
|
|
866
|
+
def exec(self, command:str=None):
|
|
867
|
+
if not self._in_raw_repl:
|
|
868
|
+
self._enter_repl()
|
|
869
|
+
|
|
870
|
+
try:
|
|
871
|
+
command = textwrap.dedent(command)
|
|
872
|
+
return self._exec(command)
|
|
873
|
+
except ProtocolError:
|
|
874
|
+
raise
|
|
875
|
+
finally:
|
|
876
|
+
pass
|
|
877
|
+
|
|
878
|
+
def run(self, local, interactive:bool=False, echo:bool=False):
|
|
879
|
+
if not interactive and echo:
|
|
880
|
+
import typer
|
|
881
|
+
|
|
882
|
+
raise typer.BadParameter("Option chaining error: -n and -e can only be used once, not multiple times.")
|
|
883
|
+
|
|
884
|
+
with open(local, "rb") as f:
|
|
885
|
+
data = f.read()
|
|
886
|
+
|
|
887
|
+
if not data or len(data) == 0:
|
|
888
|
+
return
|
|
889
|
+
|
|
890
|
+
if not self._in_raw_repl:
|
|
891
|
+
self._enter_repl()
|
|
892
|
+
|
|
893
|
+
try:
|
|
894
|
+
self._soft_reset()
|
|
895
|
+
self._exec(data, interactive, echo, detach=not interactive)
|
|
896
|
+
if interactive:
|
|
897
|
+
self._drain_eof(max_ms=200)
|
|
898
|
+
except Exception:
|
|
899
|
+
self._leave_repl()
|
|
900
|
+
raise
|
|
901
|
+
|
|
902
|
+
def close(self):
|
|
903
|
+
self.transport.close()
|
|
904
|
+
|
|
905
|
+
def reset(self):
|
|
906
|
+
self._in_raw_repl = False
|
|
907
|
+
self._enter_repl()
|
|
908
|
+
|
|
909
|
+
try:
|
|
910
|
+
self._soft_reset()
|
|
911
|
+
except Exception:
|
|
912
|
+
self._leave_repl()
|
|
913
|
+
raise
|
|
914
|
+
|
|
915
|
+
def repl(self):
|
|
916
|
+
import time
|
|
917
|
+
|
|
918
|
+
def _write_with_timeout(data: bytes, timeout_sec: float = 1.5) -> bool:
|
|
919
|
+
result = {"ok": False, "err": None}
|
|
920
|
+
|
|
921
|
+
def _writer():
|
|
922
|
+
try:
|
|
923
|
+
self.transport.write(data)
|
|
924
|
+
result["ok"] = True
|
|
925
|
+
except Exception as e:
|
|
926
|
+
result["err"] = e
|
|
927
|
+
|
|
928
|
+
t = threading.Thread(target=_writer, daemon=True)
|
|
929
|
+
t.start()
|
|
930
|
+
t.join(timeout=timeout_sec)
|
|
931
|
+
if t.is_alive():
|
|
932
|
+
return False
|
|
933
|
+
return bool(result["ok"]) and result["err"] is None
|
|
934
|
+
|
|
935
|
+
self.serial_reader_running = True
|
|
936
|
+
self.serial_out_put_enable = False
|
|
937
|
+
self.serial_out_put_count = 0
|
|
938
|
+
self._repl_prompt_detected = False
|
|
939
|
+
|
|
940
|
+
repl_thread = threading.Thread(target=self._repl_serial_to_stdout, daemon=True, name='REPL')
|
|
941
|
+
repl_thread.start()
|
|
942
|
+
|
|
943
|
+
time.sleep(0.05)
|
|
944
|
+
|
|
945
|
+
if not _write_with_timeout(CTRL_B):
|
|
946
|
+
self.serial_reader_running = False
|
|
947
|
+
print('')
|
|
948
|
+
return
|
|
949
|
+
time.sleep(0.05)
|
|
950
|
+
if not _write_with_timeout(CTRL_C):
|
|
951
|
+
self.serial_reader_running = False
|
|
952
|
+
print('')
|
|
953
|
+
return
|
|
954
|
+
time.sleep(0.1)
|
|
955
|
+
|
|
956
|
+
drain_timeout = time.monotonic() + 0.5
|
|
957
|
+
while time.monotonic() < drain_timeout:
|
|
958
|
+
if self.transport.in_waiting() > 0:
|
|
959
|
+
self.transport.read(self.transport.in_waiting())
|
|
960
|
+
time.sleep(0.02)
|
|
961
|
+
else:
|
|
962
|
+
time.sleep(0.05)
|
|
963
|
+
if self.transport.in_waiting() == 0:
|
|
964
|
+
break
|
|
965
|
+
|
|
966
|
+
self.serial_out_put_enable = True
|
|
967
|
+
self.serial_out_put_count = 1
|
|
968
|
+
if not _write_with_timeout(b'\r'):
|
|
969
|
+
self.serial_reader_running = False
|
|
970
|
+
print('')
|
|
971
|
+
return
|
|
972
|
+
|
|
973
|
+
prompt_timeout = time.monotonic() + 1.0
|
|
974
|
+
while time.monotonic() < prompt_timeout:
|
|
975
|
+
time.sleep(0.05)
|
|
976
|
+
if self._repl_prompt_detected:
|
|
977
|
+
break
|
|
978
|
+
|
|
979
|
+
recent_chars = bytearray(6)
|
|
980
|
+
recent_len = 0
|
|
981
|
+
|
|
982
|
+
while True:
|
|
983
|
+
if self.serial_reader_running and not repl_thread.is_alive():
|
|
984
|
+
break
|
|
985
|
+
|
|
986
|
+
char = getch()
|
|
987
|
+
if not char:
|
|
988
|
+
continue
|
|
989
|
+
|
|
990
|
+
if char == b'\x07':
|
|
991
|
+
self.serial_out_put_enable = False
|
|
992
|
+
continue
|
|
993
|
+
elif char == b'\x0F':
|
|
994
|
+
self.serial_out_put_enable = True
|
|
995
|
+
self.serial_out_put_count = 0
|
|
996
|
+
continue
|
|
997
|
+
elif char == b'\x00' or not char:
|
|
998
|
+
continue
|
|
999
|
+
|
|
1000
|
+
if char == b'\r' or char == b'\n':
|
|
1001
|
+
if recent_len >= 4:
|
|
1002
|
+
cmd = bytes(recent_chars[:recent_len]).strip().lower()
|
|
1003
|
+
if cmd in (b'exit', b'exit()'):
|
|
1004
|
+
break
|
|
1005
|
+
recent_len = 0
|
|
1006
|
+
elif char == b'\x7f' or char == b'\x08':
|
|
1007
|
+
if recent_len > 0:
|
|
1008
|
+
recent_len -= 1
|
|
1009
|
+
elif char >= b' ' and len(char) == 1:
|
|
1010
|
+
if recent_len < 6:
|
|
1011
|
+
recent_chars[recent_len] = char[0]
|
|
1012
|
+
recent_len += 1
|
|
1013
|
+
else:
|
|
1014
|
+
recent_chars[:-1] = recent_chars[1:]
|
|
1015
|
+
recent_chars[5] = char[0]
|
|
1016
|
+
|
|
1017
|
+
try:
|
|
1018
|
+
if not _write_with_timeout(b'\r' if char == b'\n' else char):
|
|
1019
|
+
break
|
|
1020
|
+
except Exception:
|
|
1021
|
+
break
|
|
1022
|
+
|
|
1023
|
+
self.serial_reader_running = False
|
|
1024
|
+
if repl_thread.is_alive():
|
|
1025
|
+
try:
|
|
1026
|
+
repl_thread.join(timeout=0.2)
|
|
1027
|
+
except Exception:
|
|
1028
|
+
pass
|
|
1029
|
+
print('')
|