replx 1.2__tar.gz → 1.3__tar.gz
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.
- replx-1.3/PKG-INFO +25 -0
- {replx-1.2 → replx-1.3}/replx/__init__.py +1 -1
- {replx-1.2 → replx-1.3}/replx/cli/agent/client/core.py +1 -5
- {replx-1.2 → replx-1.3}/replx/cli/agent/client/session.py +0 -25
- {replx-1.2 → replx-1.3}/replx/cli/agent/protocol.py +0 -3
- {replx-1.2 → replx-1.3}/replx/cli/agent/server/connection_manager.py +1 -84
- {replx-1.2 → replx-1.3}/replx/cli/agent/server/core.py +0 -7
- {replx-1.2 → replx-1.3}/replx/cli/agent/server/handlers/exec.py +7 -32
- {replx-1.2 → replx-1.3}/replx/cli/agent/server/handlers/filesystem.py +16 -21
- {replx-1.2 → replx-1.3}/replx/cli/agent/server/handlers/session.py +9 -7
- {replx-1.2 → replx-1.3}/replx/cli/agent/server/handlers/transfer.py +1 -15
- {replx-1.2 → replx-1.3}/replx/cli/agent/server/session_manager.py +3 -13
- {replx-1.2 → replx-1.3}/replx/cli/app.py +1 -64
- {replx-1.2 → replx-1.3}/replx/cli/commands/device.py +1 -0
- {replx-1.2 → replx-1.3}/replx/cli/config.py +3 -179
- {replx-1.2 → replx-1.3}/replx/cli/connection.py +4 -78
- {replx-1.2 → replx-1.3}/replx/cli/helpers/__init__.py +4 -17
- {replx-1.2 → replx-1.3}/replx/cli/helpers/compiler.py +0 -29
- {replx-1.2 → replx-1.3}/replx/cli/helpers/environment.py +0 -13
- {replx-1.2 → replx-1.3}/replx/cli/helpers/output.py +0 -58
- {replx-1.2 → replx-1.3}/replx/cli/helpers/registry.py +6 -139
- {replx-1.2 → replx-1.3}/replx/cli/helpers/scanner.py +3 -83
- {replx-1.2 → replx-1.3}/replx/cli/helpers/updater.py +0 -4
- {replx-1.2 → replx-1.3}/replx/terminal.py +1 -3
- {replx-1.2 → replx-1.3}/replx/transport/__init__.py +0 -11
- {replx-1.2 → replx-1.3}/replx/transport/serial.py +0 -10
- {replx-1.2 → replx-1.3}/replx/utils/__init__.py +0 -1
- {replx-1.2 → replx-1.3}/replx/utils/device_info.py +9 -23
- replx-1.3/replx.egg-info/PKG-INFO +25 -0
- {replx-1.2 → replx-1.3}/replx.egg-info/SOURCES.txt +0 -6
- replx-1.2/PKG-INFO +0 -101
- replx-1.2/README.md +0 -75
- replx-1.2/replx/tests/test_agent_port_canonicalization.py +0 -43
- replx-1.2/replx/tests/test_disconnect_cleanup.py +0 -46
- replx-1.2/replx/tests/test_pkg_search_scope_filter.py +0 -82
- replx-1.2/replx/tests/test_shutdown_status_message.py +0 -56
- replx-1.2/replx/tests/test_windows_com_port_normalization.py +0 -69
- replx-1.2/replx.egg-info/PKG-INFO +0 -101
- {replx-1.2 → replx-1.3}/LICENSE +0 -0
- {replx-1.2 → replx-1.3}/pyproject.toml +0 -0
- {replx-1.2 → replx-1.3}/replx/cli/__init__.py +0 -0
- {replx-1.2 → replx-1.3}/replx/cli/agent/__init__.py +0 -0
- {replx-1.2 → replx-1.3}/replx/cli/agent/client/__init__.py +0 -0
- {replx-1.2 → replx-1.3}/replx/cli/agent/server/__init__.py +0 -0
- {replx-1.2 → replx-1.3}/replx/cli/agent/server/__main__.py +0 -0
- {replx-1.2 → replx-1.3}/replx/cli/agent/server/command_dispatcher.py +0 -0
- {replx-1.2 → replx-1.3}/replx/cli/agent/server/handlers/__init__.py +0 -0
- {replx-1.2 → replx-1.3}/replx/cli/agent/server/handlers/repl.py +0 -0
- {replx-1.2 → replx-1.3}/replx/cli/commands/__init__.py +0 -0
- {replx-1.2 → replx-1.3}/replx/cli/commands/exec.py +0 -0
- {replx-1.2 → replx-1.3}/replx/cli/commands/file.py +0 -0
- {replx-1.2 → replx-1.3}/replx/cli/commands/firmware.py +0 -0
- {replx-1.2 → replx-1.3}/replx/cli/commands/package.py +0 -0
- {replx-1.2 → replx-1.3}/replx/cli/commands/utility.py +0 -0
- {replx-1.2 → replx-1.3}/replx/cli/helpers/store.py +0 -0
- {replx-1.2 → replx-1.3}/replx/commands.py +0 -0
- {replx-1.2 → replx-1.3}/replx/protocol/__init__.py +0 -0
- {replx-1.2 → replx-1.3}/replx/protocol/repl.py +0 -0
- {replx-1.2 → replx-1.3}/replx/protocol/storage.py +0 -0
- {replx-1.2 → replx-1.3}/replx/tests/__init__.py +0 -0
- {replx-1.2 → replx-1.3}/replx/tests/test_compiler_arch.py +0 -0
- {replx-1.2 → replx-1.3}/replx/tests/test_connection_info_lookup.py +0 -0
- {replx-1.2 → replx-1.3}/replx/tests/test_device_info_esp_multi_core.py +0 -0
- {replx-1.2 → replx-1.3}/replx/tests/test_pkg_local_version.py +0 -0
- {replx-1.2 → replx-1.3}/replx/tests/test_session_id_fallback.py +0 -0
- {replx-1.2 → replx-1.3}/replx/transport/base.py +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm/_thread.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm/aioble/__init__.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm/array.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm/asyncio/__init__.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm/binascii.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm/bluetooth.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm/builtins.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm/cmath.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm/collections.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm/cryptolib.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm/deflate.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm/errno.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm/framebuf.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm/gc.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm/hashlib.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm/heapq.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm/io.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm/json.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm/lwip.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm/machine.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm/math.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm/micropython.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm/mip/__init__.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm/network.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm/ntptime.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm/os.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm/platform.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm/random.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm/re.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm/requests/__init__.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm/select.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm/socket.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm/ssl.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm/struct.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm/sys.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm/time.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm/tls.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm/uasyncio.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm/uctypes.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm/urequests.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm/vfs.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/binascii.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/errno.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/hashlib.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/io.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/json.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/machine.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/math.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/micropython.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/network.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/os.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/select.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/socket.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/ssl.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/struct.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/sys.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/time.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/ubinascii.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/ucryptolib.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/uerrno.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/uhashlib.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/uio.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/ujson.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/umachine.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/uos.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/uselect.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/usocket.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/ussl.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/ustruct.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/utime.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/core/ESP32/aioespnow.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/core/ESP32/esp.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/core/ESP32/esp32.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/core/ESP32/espnow.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/core/MIMXRT1062DVJ6A/mimxrt.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/typehints/core/RP2350/rp2.pyi +0 -0
- {replx-1.2 → replx-1.3}/replx/utils/constants.py +0 -0
- {replx-1.2 → replx-1.3}/replx/utils/exceptions.py +0 -0
- {replx-1.2 → replx-1.3}/replx.egg-info/dependency_links.txt +0 -0
- {replx-1.2 → replx-1.3}/replx.egg-info/entry_points.txt +0 -0
- {replx-1.2 → replx-1.3}/replx.egg-info/requires.txt +0 -0
- {replx-1.2 → replx-1.3}/replx.egg-info/top_level.txt +0 -0
- {replx-1.2 → replx-1.3}/setup.cfg +0 -0
replx-1.3/PKG-INFO
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: replx
|
|
3
|
+
Version: 1.3
|
|
4
|
+
Summary: replx is a fast, modern MicroPython CLI: turbo REPL, robust file sync (put/get), project install, mpy-cross integration, and smart port discovery.
|
|
5
|
+
Author-email: "chanmin.park" <devcamp@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/PlanXLab/replx
|
|
8
|
+
Project-URL: Repository, https://github.com/PlanXLab/replx
|
|
9
|
+
Project-URL: Issues, https://github.com/PlanXLab/replx/issues
|
|
10
|
+
Keywords: micropython,repl,serial,pyserial,typer,mpy-cross,deploy
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Topic :: Software Development :: Embedded Systems
|
|
16
|
+
Classifier: Topic :: System :: Hardware :: Universal Serial Bus (USB)
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: typer>=0.12
|
|
21
|
+
Requires-Dist: rich>=13.0
|
|
22
|
+
Requires-Dist: pyserial>=3.5
|
|
23
|
+
Requires-Dist: mpy-cross>=1.26
|
|
24
|
+
Requires-Dist: psutil>=5.9.0
|
|
25
|
+
Dynamic: license-file
|
|
@@ -59,7 +59,6 @@ class AgentClient:
|
|
|
59
59
|
|
|
60
60
|
response = None
|
|
61
61
|
|
|
62
|
-
# For short timeouts (< 1s), don't retry to fail fast (e.g., ping/agent check)
|
|
63
62
|
max_attempts = 1 if effective_timeout < 1.0 else self.MAX_RETRIES
|
|
64
63
|
|
|
65
64
|
for attempt in range(max_attempts):
|
|
@@ -324,7 +323,6 @@ class AgentClient:
|
|
|
324
323
|
|
|
325
324
|
if background:
|
|
326
325
|
if sys.platform == 'win32':
|
|
327
|
-
# Windows: Use DETACHED_PROCESS to run without console
|
|
328
326
|
startupinfo = subprocess.STARTUPINFO()
|
|
329
327
|
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
|
330
328
|
startupinfo.wShowWindow = 0
|
|
@@ -337,7 +335,6 @@ class AgentClient:
|
|
|
337
335
|
startupinfo=startupinfo
|
|
338
336
|
)
|
|
339
337
|
else:
|
|
340
|
-
# Unix: Use start_new_session for proper daemon behavior
|
|
341
338
|
subprocess.Popen(
|
|
342
339
|
cmd,
|
|
343
340
|
stdout=subprocess.DEVNULL,
|
|
@@ -367,10 +364,9 @@ class AgentClient:
|
|
|
367
364
|
except Exception:
|
|
368
365
|
pass
|
|
369
366
|
|
|
370
|
-
# Wait for agent to stop, but with faster polling
|
|
371
367
|
start_time = time.time()
|
|
372
368
|
while time.time() - start_time < timeout:
|
|
373
|
-
time.sleep(0.05)
|
|
369
|
+
time.sleep(0.05)
|
|
374
370
|
if not AgentClient.is_agent_running(port=port):
|
|
375
371
|
return True
|
|
376
372
|
|
|
@@ -3,7 +3,6 @@ from typing import Optional
|
|
|
3
3
|
import psutil
|
|
4
4
|
|
|
5
5
|
def _find_terminal_process() -> Optional[dict]:
|
|
6
|
-
# Prefer actual shell processes (per-terminal) over host IDE processes (shared).
|
|
7
6
|
shell_names = {
|
|
8
7
|
'powershell.exe', 'pwsh.exe', 'cmd.exe', 'bash.exe', 'zsh.exe', 'sh.exe', 'fish.exe',
|
|
9
8
|
'windowsterminal.exe',
|
|
@@ -15,7 +14,6 @@ def _find_terminal_process() -> Optional[dict]:
|
|
|
15
14
|
'pycharm.exe', 'pycharm64.exe', 'idea.exe', 'idea64.exe',
|
|
16
15
|
}
|
|
17
16
|
|
|
18
|
-
# Fast path: parent pid is typically the actual terminal/shell.
|
|
19
17
|
try:
|
|
20
18
|
parent_pid = os.getppid()
|
|
21
19
|
if parent_pid and parent_pid > 0:
|
|
@@ -30,7 +28,6 @@ def _find_terminal_process() -> Optional[dict]:
|
|
|
30
28
|
'level': 0,
|
|
31
29
|
}
|
|
32
30
|
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess, OSError):
|
|
33
|
-
# Fall back to a broader traversal.
|
|
34
31
|
pass
|
|
35
32
|
|
|
36
33
|
try:
|
|
@@ -56,7 +53,6 @@ def _find_terminal_process() -> Optional[dict]:
|
|
|
56
53
|
}
|
|
57
54
|
|
|
58
55
|
if name in ide_names and best_ide is None:
|
|
59
|
-
# Keep as last resort (may be shared across terminals).
|
|
60
56
|
try:
|
|
61
57
|
best_ide = {
|
|
62
58
|
'pid': current.pid,
|
|
@@ -87,7 +83,6 @@ def _find_terminal_process() -> Optional[dict]:
|
|
|
87
83
|
return best_ide
|
|
88
84
|
|
|
89
85
|
except Exception:
|
|
90
|
-
# Never allow session id discovery to fail hard.
|
|
91
86
|
pass
|
|
92
87
|
|
|
93
88
|
return None
|
|
@@ -122,26 +117,6 @@ def _find_jupyter_kernel() -> Optional[dict]:
|
|
|
122
117
|
|
|
123
118
|
return None
|
|
124
119
|
|
|
125
|
-
def _detect_environment() -> str:
|
|
126
|
-
try:
|
|
127
|
-
__IPYTHON__
|
|
128
|
-
return 'ipython'
|
|
129
|
-
except NameError:
|
|
130
|
-
pass
|
|
131
|
-
|
|
132
|
-
env = os.environ
|
|
133
|
-
|
|
134
|
-
if any(key.startswith('JPY_') or 'JUPYTER' in key for key in env):
|
|
135
|
-
return 'jupyter'
|
|
136
|
-
|
|
137
|
-
if env.get('TERM_PROGRAM') == 'vscode':
|
|
138
|
-
return 'vscode_terminal'
|
|
139
|
-
|
|
140
|
-
if env.get('SHELL') or env.get('TERM'):
|
|
141
|
-
return 'terminal'
|
|
142
|
-
|
|
143
|
-
return 'unknown'
|
|
144
|
-
|
|
145
120
|
def get_session_id() -> int:
|
|
146
121
|
terminal = _find_terminal_process()
|
|
147
122
|
if terminal:
|
|
@@ -6,14 +6,11 @@ from typing import Dict, Any, Optional, List
|
|
|
6
6
|
|
|
7
7
|
from replx.utils.constants import MAX_PAYLOAD_SIZE
|
|
8
8
|
|
|
9
|
-
# Protocol constants
|
|
10
9
|
_MAGIC = b'RPLX'
|
|
11
10
|
_VERSION = 1
|
|
12
11
|
|
|
13
12
|
|
|
14
13
|
class AgentProtocol:
|
|
15
|
-
"""UDP protocol for agent client-server communication."""
|
|
16
|
-
|
|
17
14
|
@staticmethod
|
|
18
15
|
def encode_message(msg: Dict[str, Any]) -> bytes:
|
|
19
16
|
payload = json.dumps(msg).encode('utf-8')
|
|
@@ -11,12 +11,6 @@ from replx.utils.exceptions import TransportError
|
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
def _detect_device_info(transport, core: str, device: str = None) -> Tuple[str, str, str, str]:
|
|
14
|
-
"""Detect device information from transport.
|
|
15
|
-
|
|
16
|
-
Returns:
|
|
17
|
-
Tuple of (version, core, device, manufacturer)
|
|
18
|
-
"""
|
|
19
|
-
# Use shorter delays on Unix for faster failure on invalid ports
|
|
20
14
|
delay1 = 0.05 if sys.platform != "win32" else 0.1
|
|
21
15
|
delay2 = 0.1 if sys.platform != "win32" else 0.2
|
|
22
16
|
delay3 = 0.1 if sys.platform != "win32" else 0.2
|
|
@@ -168,7 +162,6 @@ class BoardConnection:
|
|
|
168
162
|
last_command_time: float = field(default_factory=time.time)
|
|
169
163
|
_busy_lock: threading.Lock = field(default_factory=threading.Lock)
|
|
170
164
|
|
|
171
|
-
# Detached script state (per-connection)
|
|
172
165
|
detached_running: bool = False
|
|
173
166
|
_detached_lock: threading.Lock = field(default_factory=threading.Lock)
|
|
174
167
|
_drain_thread: Optional[threading.Thread] = field(default=None, repr=False)
|
|
@@ -208,7 +201,6 @@ class BoardConnection:
|
|
|
208
201
|
self.busy_client = None
|
|
209
202
|
|
|
210
203
|
def stop_detached(self):
|
|
211
|
-
"""Stop detached script and drain thread for this connection."""
|
|
212
204
|
with self._detached_lock:
|
|
213
205
|
self.detached_running = False
|
|
214
206
|
|
|
@@ -221,7 +213,6 @@ class ConnectionManager:
|
|
|
221
213
|
self._connections: Dict[str, BoardConnection] = {}
|
|
222
214
|
self._connections_lock = threading.RLock()
|
|
223
215
|
self._default_port: Optional[str] = None
|
|
224
|
-
# Cache: maps board_id -> {serial_port}
|
|
225
216
|
self._board_id_cache: Dict[str, Dict[str, str]] = {}
|
|
226
217
|
self._cache_lock = threading.Lock()
|
|
227
218
|
|
|
@@ -239,11 +230,6 @@ class ConnectionManager:
|
|
|
239
230
|
|
|
240
231
|
@staticmethod
|
|
241
232
|
def _canon_port(port: Optional[str]) -> Optional[str]:
|
|
242
|
-
"""Canonicalize port keys for internal storage/lookup.
|
|
243
|
-
|
|
244
|
-
On Windows, COM ports are case-insensitive; we canonicalize COMx to
|
|
245
|
-
uppercase so all commands behave consistently.
|
|
246
|
-
"""
|
|
247
233
|
if port is None:
|
|
248
234
|
return None
|
|
249
235
|
p = str(port).strip()
|
|
@@ -254,7 +240,6 @@ class ConnectionManager:
|
|
|
254
240
|
return p
|
|
255
241
|
|
|
256
242
|
def _resolve_existing_key(self, port: str) -> Optional[str]:
|
|
257
|
-
"""Return the stored key matching `port`, possibly case-insensitively on Windows."""
|
|
258
243
|
if port is None:
|
|
259
244
|
return None
|
|
260
245
|
p = self._canon_port(port)
|
|
@@ -306,7 +291,6 @@ class ConnectionManager:
|
|
|
306
291
|
device: str = None,
|
|
307
292
|
baudrate: int = 115200
|
|
308
293
|
) -> Tuple[BoardConnection, Optional[str]]:
|
|
309
|
-
# Store port
|
|
310
294
|
original_port = str(port).strip() if port is not None else ""
|
|
311
295
|
port_key = self._canon_port(original_port)
|
|
312
296
|
|
|
@@ -328,13 +312,10 @@ class ConnectionManager:
|
|
|
328
312
|
repl_protocol = ReplProtocol(transport)
|
|
329
313
|
|
|
330
314
|
device_root_fs = "/"
|
|
331
|
-
# Skip filesystem check if detection already failed (version is "?")
|
|
332
|
-
# This speeds up failure on invalid ports
|
|
333
315
|
if version != "?":
|
|
334
316
|
try:
|
|
335
317
|
result = repl_protocol.exec("import os; print(os.getcwd())")
|
|
336
318
|
if result:
|
|
337
|
-
# exec() returns bytes, decode to string
|
|
338
319
|
if isinstance(result, bytes):
|
|
339
320
|
result = result.decode('utf-8', errors='ignore')
|
|
340
321
|
cwd = result.strip() if result else "/"
|
|
@@ -357,7 +338,7 @@ class ConnectionManager:
|
|
|
357
338
|
)
|
|
358
339
|
|
|
359
340
|
conn = BoardConnection(
|
|
360
|
-
port=port_key,
|
|
341
|
+
port=port_key,
|
|
361
342
|
repl_protocol=repl_protocol,
|
|
362
343
|
file_system=file_system,
|
|
363
344
|
core=detected_core,
|
|
@@ -376,35 +357,21 @@ class ConnectionManager:
|
|
|
376
357
|
return None, str(e)
|
|
377
358
|
|
|
378
359
|
def ensure_board_id(self, port: str) -> Optional[str]:
|
|
379
|
-
"""
|
|
380
|
-
Lazily query and cache the board's unique ID.
|
|
381
|
-
Uses machine.unique_id().hex() to get a consistent ID.
|
|
382
|
-
|
|
383
|
-
Args:
|
|
384
|
-
port: The connection key
|
|
385
|
-
|
|
386
|
-
Returns:
|
|
387
|
-
The board_id string, or None if query failed
|
|
388
|
-
"""
|
|
389
360
|
conn = self.get_connection(port)
|
|
390
361
|
if not conn or not conn.repl_protocol:
|
|
391
362
|
return None
|
|
392
363
|
|
|
393
|
-
# Return cached value if already queried
|
|
394
364
|
if conn.board_id is not None:
|
|
395
365
|
return conn.board_id
|
|
396
366
|
|
|
397
367
|
try:
|
|
398
|
-
# Query machine.unique_id().hex()
|
|
399
368
|
result = conn.repl_protocol.exec("import machine; print(machine.unique_id().hex())")
|
|
400
369
|
if result:
|
|
401
|
-
# exec() returns bytes, decode to string
|
|
402
370
|
if isinstance(result, bytes):
|
|
403
371
|
result = result.decode('utf-8', errors='ignore')
|
|
404
372
|
result = result.strip()
|
|
405
373
|
if result:
|
|
406
374
|
conn.board_id = result
|
|
407
|
-
# Update board_id cache
|
|
408
375
|
self._update_board_id_cache(port, conn.board_id)
|
|
409
376
|
return conn.board_id
|
|
410
377
|
except Exception:
|
|
@@ -413,9 +380,6 @@ class ConnectionManager:
|
|
|
413
380
|
return None
|
|
414
381
|
|
|
415
382
|
def _update_board_id_cache(self, port: str, board_id: str) -> None:
|
|
416
|
-
"""
|
|
417
|
-
Update the board_id cache with port mapping.
|
|
418
|
-
"""
|
|
419
383
|
with self._cache_lock:
|
|
420
384
|
if board_id not in self._board_id_cache:
|
|
421
385
|
self._board_id_cache[board_id] = {}
|
|
@@ -423,37 +387,22 @@ class ConnectionManager:
|
|
|
423
387
|
self._board_id_cache[board_id]["serial_port"] = self._canon_port(port)
|
|
424
388
|
|
|
425
389
|
def find_conflicting_by_board_id(self, port: str) -> Optional[str]:
|
|
426
|
-
"""
|
|
427
|
-
Find conflicting connection by comparing board IDs.
|
|
428
|
-
This is called after ensure_board_id() to detect if the same board
|
|
429
|
-
is already connected via a different transport.
|
|
430
|
-
|
|
431
|
-
Args:
|
|
432
|
-
port: The current connection key
|
|
433
|
-
|
|
434
|
-
Returns:
|
|
435
|
-
The conflicting connection's port key, or None if no conflict
|
|
436
|
-
"""
|
|
437
390
|
conn = self.get_connection(port)
|
|
438
391
|
if not conn or not conn.board_id:
|
|
439
392
|
return None
|
|
440
393
|
|
|
441
394
|
with self._connections_lock:
|
|
442
395
|
for key, other_conn in self._connections.items():
|
|
443
|
-
# Skip self
|
|
444
396
|
if key == port:
|
|
445
397
|
continue
|
|
446
398
|
|
|
447
|
-
# Skip disconnected
|
|
448
399
|
if not other_conn.is_connected():
|
|
449
400
|
continue
|
|
450
401
|
|
|
451
|
-
# Ensure other connection's board_id is queried
|
|
452
402
|
if other_conn.board_id is None:
|
|
453
403
|
try:
|
|
454
404
|
result = other_conn.repl_protocol.exec("import machine; print(machine.unique_id().hex())")
|
|
455
405
|
if result:
|
|
456
|
-
# exec() returns bytes, decode to string
|
|
457
406
|
if isinstance(result, bytes):
|
|
458
407
|
result = result.decode('utf-8', errors='ignore')
|
|
459
408
|
result = result.strip()
|
|
@@ -462,46 +411,28 @@ class ConnectionManager:
|
|
|
462
411
|
except Exception:
|
|
463
412
|
continue
|
|
464
413
|
|
|
465
|
-
# Compare board IDs
|
|
466
414
|
if other_conn.board_id and other_conn.board_id == conn.board_id:
|
|
467
415
|
return key
|
|
468
416
|
|
|
469
417
|
return None
|
|
470
418
|
|
|
471
419
|
def resolve_board_conflict(self, port: str) -> Optional[str]:
|
|
472
|
-
"""
|
|
473
|
-
Check for board conflicts using board_id and automatically disconnect
|
|
474
|
-
the conflicting connection.
|
|
475
|
-
|
|
476
|
-
Args:
|
|
477
|
-
port: The current connection key
|
|
478
|
-
|
|
479
|
-
Returns:
|
|
480
|
-
The disconnected port key, or None if no conflict was found/resolved
|
|
481
|
-
"""
|
|
482
|
-
# First, ensure board_id is queried for this connection
|
|
483
420
|
board_id = self.ensure_board_id(port)
|
|
484
421
|
if not board_id:
|
|
485
422
|
return None
|
|
486
423
|
|
|
487
|
-
# Find any conflicting connection
|
|
488
424
|
conflicting_port = self.find_conflicting_by_board_id(port)
|
|
489
425
|
if conflicting_port:
|
|
490
|
-
# Auto-disconnect the older connection
|
|
491
426
|
self.disconnect(conflicting_port)
|
|
492
427
|
|
|
493
|
-
# Reset REPL state of the new connection
|
|
494
428
|
conn = self.get_connection(port)
|
|
495
429
|
if conn and conn.repl_protocol:
|
|
496
430
|
try:
|
|
497
431
|
transport = conn.repl_protocol._transport
|
|
498
|
-
# Send Ctrl+C to interrupt any ongoing operation
|
|
499
432
|
transport.write(b'\x03')
|
|
500
433
|
time.sleep(0.05)
|
|
501
|
-
# Send Ctrl+B to enter Normal mode (exit Raw REPL if in it)
|
|
502
434
|
transport.write(b'\x02')
|
|
503
435
|
time.sleep(0.1)
|
|
504
|
-
# Clear any pending data
|
|
505
436
|
transport.reset_input_buffer()
|
|
506
437
|
except Exception:
|
|
507
438
|
pass
|
|
@@ -519,19 +450,15 @@ class ConnectionManager:
|
|
|
519
450
|
|
|
520
451
|
conn = self._connections.pop(key)
|
|
521
452
|
|
|
522
|
-
# Stop detached script first (this waits for drain thread)
|
|
523
453
|
conn.stop_detached()
|
|
524
454
|
|
|
525
|
-
# Stop REPL/interactive workers before transport close to release serial handle promptly.
|
|
526
455
|
if conn.repl.active:
|
|
527
456
|
conn.repl.stop()
|
|
528
457
|
if conn.interactive.active:
|
|
529
458
|
conn.interactive.stop()
|
|
530
459
|
|
|
531
|
-
# Release busy state
|
|
532
460
|
conn.release()
|
|
533
461
|
|
|
534
|
-
# Close transport outside the lock to avoid potential deadlocks
|
|
535
462
|
if conn.repl_protocol:
|
|
536
463
|
try:
|
|
537
464
|
transport = conn.repl_protocol._transport
|
|
@@ -592,13 +519,6 @@ class ConnectionManager:
|
|
|
592
519
|
return conn.busy if conn else False
|
|
593
520
|
|
|
594
521
|
def check_health(self, port: str) -> bool:
|
|
595
|
-
"""Check if the connection to the specified port is still healthy.
|
|
596
|
-
|
|
597
|
-
This method actively tests the serial port to detect disconnection.
|
|
598
|
-
|
|
599
|
-
Returns:
|
|
600
|
-
True if the connection is healthy, False otherwise.
|
|
601
|
-
"""
|
|
602
522
|
conn = self.get_connection(port)
|
|
603
523
|
if not conn or not conn.repl_protocol:
|
|
604
524
|
return False
|
|
@@ -608,14 +528,11 @@ class ConnectionManager:
|
|
|
608
528
|
if not transport:
|
|
609
529
|
return False
|
|
610
530
|
|
|
611
|
-
# Use check_connection which actively probes the port
|
|
612
531
|
if hasattr(transport, 'check_connection'):
|
|
613
532
|
return transport.check_connection()
|
|
614
533
|
|
|
615
|
-
# Fallback: check is_open property
|
|
616
534
|
return transport.is_open
|
|
617
535
|
except TransportError:
|
|
618
|
-
# Serial port has been disconnected
|
|
619
536
|
return False
|
|
620
537
|
except Exception:
|
|
621
538
|
return False
|
|
@@ -217,8 +217,6 @@ class AgentServer(
|
|
|
217
217
|
if not port:
|
|
218
218
|
raise ValueError("Port is required")
|
|
219
219
|
|
|
220
|
-
# Use ConnectionManager's create_serial_connection which properly detects device info
|
|
221
|
-
# before entering raw REPL mode
|
|
222
220
|
board_conn, error = self.connection_manager.create_serial_connection(
|
|
223
221
|
port=port,
|
|
224
222
|
core=core,
|
|
@@ -278,12 +276,7 @@ class AgentServer(
|
|
|
278
276
|
gc_counter = 0
|
|
279
277
|
while self.running:
|
|
280
278
|
time.sleep(HEARTBEAT_INTERVAL)
|
|
281
|
-
# IMPORTANT: While REPL/interactive session is active, suspend all
|
|
282
|
-
# heartbeat maintenance work (including zombie cleanup) to avoid
|
|
283
|
-
# interfering with long-running terminal interactions.
|
|
284
279
|
all_conns = self.connection_manager.get_all_connections()
|
|
285
|
-
if all_conns and any(conn.interactive.active or conn.repl.active for conn in all_conns.values()):
|
|
286
|
-
continue
|
|
287
280
|
|
|
288
281
|
zombie_check_counter += 1
|
|
289
282
|
gc_counter += 1
|
|
@@ -12,11 +12,6 @@ from ..connection_manager import BoardConnection
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
def _normalize_port(port: str) -> str:
|
|
15
|
-
"""Normalize port name for comparison.
|
|
16
|
-
|
|
17
|
-
Windows: case-insensitive (COM10 == com10)
|
|
18
|
-
Linux/macOS: case-sensitive (/dev/ttyACM0 != /dev/TTYACM0)
|
|
19
|
-
"""
|
|
20
15
|
if sys.platform.startswith("win"):
|
|
21
16
|
return port.upper()
|
|
22
17
|
else:
|
|
@@ -24,16 +19,13 @@ def _normalize_port(port: str) -> str:
|
|
|
24
19
|
|
|
25
20
|
|
|
26
21
|
def _find_connection_by_port(connection_manager, port: str) -> Optional[BoardConnection]:
|
|
27
|
-
"""Find a connection by port, honoring Windows' case-insensitive semantics."""
|
|
28
22
|
if not port:
|
|
29
23
|
return None
|
|
30
24
|
|
|
31
|
-
# Try exact match first (preserve original port case where possible)
|
|
32
25
|
conn = connection_manager.get_connection(port)
|
|
33
26
|
if conn:
|
|
34
27
|
return conn
|
|
35
28
|
|
|
36
|
-
# Windows: try normalized key, then case-insensitive scan
|
|
37
29
|
if sys.platform.startswith("win"):
|
|
38
30
|
normalized = _normalize_port(port)
|
|
39
31
|
conn = connection_manager.get_connection(normalized)
|
|
@@ -69,7 +61,6 @@ class ExecCommandsMixin:
|
|
|
69
61
|
elif ctx.ppid:
|
|
70
62
|
conn = self._get_active_connection(ctx.ppid)
|
|
71
63
|
|
|
72
|
-
# Check if any connection is running a detached script
|
|
73
64
|
all_connections = self.connection_manager.get_all_connections()
|
|
74
65
|
any_detached = any(c.is_detached() for c in all_connections.values())
|
|
75
66
|
|
|
@@ -92,7 +83,6 @@ class ExecCommandsMixin:
|
|
|
92
83
|
"board_id": conn.board_id
|
|
93
84
|
}
|
|
94
85
|
|
|
95
|
-
# If explicit port was requested but not found, do not fall back
|
|
96
86
|
if ctx.explicit_port:
|
|
97
87
|
return {
|
|
98
88
|
"running": True,
|
|
@@ -110,7 +100,11 @@ class ExecCommandsMixin:
|
|
|
110
100
|
}
|
|
111
101
|
|
|
112
102
|
if all_connections:
|
|
113
|
-
|
|
103
|
+
default_port = getattr(self, '_default_port', None)
|
|
104
|
+
if default_port and default_port in all_connections:
|
|
105
|
+
first_port, first_conn = default_port, all_connections[default_port]
|
|
106
|
+
else:
|
|
107
|
+
first_port, first_conn = next(iter(all_connections.items()))
|
|
114
108
|
is_this_detached = first_conn.is_detached()
|
|
115
109
|
return {
|
|
116
110
|
"running": True,
|
|
@@ -149,7 +143,6 @@ class ExecCommandsMixin:
|
|
|
149
143
|
self.connection_manager.disconnect_all()
|
|
150
144
|
self.session_manager.clear_all_sessions()
|
|
151
145
|
|
|
152
|
-
# Close socket to unblock _serve loop immediately
|
|
153
146
|
if self.server_socket:
|
|
154
147
|
try:
|
|
155
148
|
self.server_socket.close()
|
|
@@ -163,7 +156,6 @@ class ExecCommandsMixin:
|
|
|
163
156
|
if not conn or not conn.repl_protocol:
|
|
164
157
|
raise RuntimeError("Not connected")
|
|
165
158
|
|
|
166
|
-
# Stop any detached script on this connection before reset
|
|
167
159
|
if conn.is_detached():
|
|
168
160
|
self._stop_detached_script(conn)
|
|
169
161
|
|
|
@@ -215,7 +207,6 @@ class ExecCommandsMixin:
|
|
|
215
207
|
send_error("Interactive session already active on this connection")
|
|
216
208
|
return
|
|
217
209
|
|
|
218
|
-
# Reset board before running to ensure clean state
|
|
219
210
|
repl = conn.repl_protocol
|
|
220
211
|
if conn.is_detached():
|
|
221
212
|
self._stop_detached_script(conn)
|
|
@@ -245,7 +236,6 @@ class ExecCommandsMixin:
|
|
|
245
236
|
thread.start()
|
|
246
237
|
|
|
247
238
|
def _safe_reset_repl(self, repl):
|
|
248
|
-
"""Reset REPL to clean state after execution."""
|
|
249
239
|
try:
|
|
250
240
|
repl.interrupt()
|
|
251
241
|
time.sleep(0.05)
|
|
@@ -294,15 +284,13 @@ class ExecCommandsMixin:
|
|
|
294
284
|
def data_consumer(chunk: bytes):
|
|
295
285
|
if not chunk:
|
|
296
286
|
return
|
|
297
|
-
# Filter control chars and raw REPL prompt
|
|
298
287
|
filtered = chunk.replace(EOF_MARKER, b'').replace(b'\r', b'')
|
|
299
|
-
# Remove leading '>' (raw REPL prompt) from first chunk only
|
|
300
288
|
if first_chunk[0] and filtered.startswith(b'>'):
|
|
301
289
|
filtered = filtered[1:]
|
|
302
290
|
first_chunk[0] = False
|
|
303
291
|
elif first_chunk[0]:
|
|
304
292
|
first_chunk[0] = False
|
|
305
|
-
|
|
293
|
+
|
|
306
294
|
if filtered.endswith(b'>'):
|
|
307
295
|
filtered = filtered[:-1]
|
|
308
296
|
if not filtered:
|
|
@@ -360,7 +348,6 @@ class ExecCommandsMixin:
|
|
|
360
348
|
repl._exec(script_data, interactive=False, echo=False, detach=False,
|
|
361
349
|
data_consumer=data_consumer)
|
|
362
350
|
except ProtocolError as e:
|
|
363
|
-
# Store error but don't duplicate - will be sent via completed message
|
|
364
351
|
conn.interactive.error = str(e)
|
|
365
352
|
finally:
|
|
366
353
|
input_thread_running[0] = False
|
|
@@ -368,7 +355,6 @@ class ExecCommandsMixin:
|
|
|
368
355
|
flush_buffer()
|
|
369
356
|
|
|
370
357
|
conn.interactive.completed = True
|
|
371
|
-
# Send completion with error (error will be shown in panel, not duplicated in output)
|
|
372
358
|
send_stream(completed=True, error=conn.interactive.error)
|
|
373
359
|
self._safe_reset_repl(repl)
|
|
374
360
|
|
|
@@ -383,7 +369,6 @@ class ExecCommandsMixin:
|
|
|
383
369
|
finally:
|
|
384
370
|
input_thread_running[0] = False
|
|
385
371
|
flush_timer_running[0] = False
|
|
386
|
-
# Wait for threads to finish
|
|
387
372
|
if input_thread.is_alive():
|
|
388
373
|
input_thread.join(timeout=0.5)
|
|
389
374
|
conn.interactive.stop()
|
|
@@ -432,21 +417,16 @@ class ExecCommandsMixin:
|
|
|
432
417
|
|
|
433
418
|
repl = conn.repl_protocol
|
|
434
419
|
|
|
435
|
-
# Reset board before running to ensure clean state (unlike exec which preserves state)
|
|
436
|
-
# Stop any detached script first
|
|
437
420
|
if conn.is_detached():
|
|
438
421
|
self._stop_detached_script(conn)
|
|
439
422
|
|
|
440
|
-
# Perform soft reset
|
|
441
423
|
repl.reset()
|
|
442
424
|
|
|
443
|
-
# Wait for board to stabilize after reset
|
|
444
425
|
time.sleep(0.1)
|
|
445
426
|
|
|
446
|
-
# Load script
|
|
447
427
|
if script_path:
|
|
448
428
|
if not os.path.exists(script_path):
|
|
449
|
-
raise
|
|
429
|
+
raise RuntimeError(f"Script not found: {script_path}")
|
|
450
430
|
with open(script_path, 'rb') as f:
|
|
451
431
|
script_data = f.read()
|
|
452
432
|
display_name = script_path
|
|
@@ -457,19 +437,16 @@ class ExecCommandsMixin:
|
|
|
457
437
|
raise RuntimeError("Either script_path or script_content required")
|
|
458
438
|
|
|
459
439
|
if detach:
|
|
460
|
-
# Non-interactive mode: send script and return immediately
|
|
461
440
|
conn.busy = True
|
|
462
441
|
conn.busy_command = 'detached_script'
|
|
463
442
|
|
|
464
443
|
try:
|
|
465
|
-
# Ensure we're in normal REPL mode
|
|
466
444
|
try:
|
|
467
445
|
repl._leave_repl()
|
|
468
446
|
except Exception:
|
|
469
447
|
pass
|
|
470
448
|
repl._in_raw_repl = False
|
|
471
449
|
|
|
472
|
-
# Clear any pending state
|
|
473
450
|
repl.interrupt()
|
|
474
451
|
time.sleep(0.05)
|
|
475
452
|
repl.interrupt()
|
|
@@ -478,7 +455,6 @@ class ExecCommandsMixin:
|
|
|
478
455
|
time.sleep(0.1)
|
|
479
456
|
repl.drain()
|
|
480
457
|
|
|
481
|
-
# Enter paste mode and send script
|
|
482
458
|
repl.enter_paste_mode()
|
|
483
459
|
time.sleep(0.2)
|
|
484
460
|
repl.drain()
|
|
@@ -499,7 +475,6 @@ class ExecCommandsMixin:
|
|
|
499
475
|
conn.release()
|
|
500
476
|
raise RuntimeError(f"Script send failed: {e}")
|
|
501
477
|
else:
|
|
502
|
-
# Blocking mode: execute and wait for result
|
|
503
478
|
try:
|
|
504
479
|
script_code = script_data.decode('utf-8', errors='replace')
|
|
505
480
|
output = repl.exec(script_code)
|