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.
Files changed (149) hide show
  1. replx-1.3/PKG-INFO +25 -0
  2. {replx-1.2 → replx-1.3}/replx/__init__.py +1 -1
  3. {replx-1.2 → replx-1.3}/replx/cli/agent/client/core.py +1 -5
  4. {replx-1.2 → replx-1.3}/replx/cli/agent/client/session.py +0 -25
  5. {replx-1.2 → replx-1.3}/replx/cli/agent/protocol.py +0 -3
  6. {replx-1.2 → replx-1.3}/replx/cli/agent/server/connection_manager.py +1 -84
  7. {replx-1.2 → replx-1.3}/replx/cli/agent/server/core.py +0 -7
  8. {replx-1.2 → replx-1.3}/replx/cli/agent/server/handlers/exec.py +7 -32
  9. {replx-1.2 → replx-1.3}/replx/cli/agent/server/handlers/filesystem.py +16 -21
  10. {replx-1.2 → replx-1.3}/replx/cli/agent/server/handlers/session.py +9 -7
  11. {replx-1.2 → replx-1.3}/replx/cli/agent/server/handlers/transfer.py +1 -15
  12. {replx-1.2 → replx-1.3}/replx/cli/agent/server/session_manager.py +3 -13
  13. {replx-1.2 → replx-1.3}/replx/cli/app.py +1 -64
  14. {replx-1.2 → replx-1.3}/replx/cli/commands/device.py +1 -0
  15. {replx-1.2 → replx-1.3}/replx/cli/config.py +3 -179
  16. {replx-1.2 → replx-1.3}/replx/cli/connection.py +4 -78
  17. {replx-1.2 → replx-1.3}/replx/cli/helpers/__init__.py +4 -17
  18. {replx-1.2 → replx-1.3}/replx/cli/helpers/compiler.py +0 -29
  19. {replx-1.2 → replx-1.3}/replx/cli/helpers/environment.py +0 -13
  20. {replx-1.2 → replx-1.3}/replx/cli/helpers/output.py +0 -58
  21. {replx-1.2 → replx-1.3}/replx/cli/helpers/registry.py +6 -139
  22. {replx-1.2 → replx-1.3}/replx/cli/helpers/scanner.py +3 -83
  23. {replx-1.2 → replx-1.3}/replx/cli/helpers/updater.py +0 -4
  24. {replx-1.2 → replx-1.3}/replx/terminal.py +1 -3
  25. {replx-1.2 → replx-1.3}/replx/transport/__init__.py +0 -11
  26. {replx-1.2 → replx-1.3}/replx/transport/serial.py +0 -10
  27. {replx-1.2 → replx-1.3}/replx/utils/__init__.py +0 -1
  28. {replx-1.2 → replx-1.3}/replx/utils/device_info.py +9 -23
  29. replx-1.3/replx.egg-info/PKG-INFO +25 -0
  30. {replx-1.2 → replx-1.3}/replx.egg-info/SOURCES.txt +0 -6
  31. replx-1.2/PKG-INFO +0 -101
  32. replx-1.2/README.md +0 -75
  33. replx-1.2/replx/tests/test_agent_port_canonicalization.py +0 -43
  34. replx-1.2/replx/tests/test_disconnect_cleanup.py +0 -46
  35. replx-1.2/replx/tests/test_pkg_search_scope_filter.py +0 -82
  36. replx-1.2/replx/tests/test_shutdown_status_message.py +0 -56
  37. replx-1.2/replx/tests/test_windows_com_port_normalization.py +0 -69
  38. replx-1.2/replx.egg-info/PKG-INFO +0 -101
  39. {replx-1.2 → replx-1.3}/LICENSE +0 -0
  40. {replx-1.2 → replx-1.3}/pyproject.toml +0 -0
  41. {replx-1.2 → replx-1.3}/replx/cli/__init__.py +0 -0
  42. {replx-1.2 → replx-1.3}/replx/cli/agent/__init__.py +0 -0
  43. {replx-1.2 → replx-1.3}/replx/cli/agent/client/__init__.py +0 -0
  44. {replx-1.2 → replx-1.3}/replx/cli/agent/server/__init__.py +0 -0
  45. {replx-1.2 → replx-1.3}/replx/cli/agent/server/__main__.py +0 -0
  46. {replx-1.2 → replx-1.3}/replx/cli/agent/server/command_dispatcher.py +0 -0
  47. {replx-1.2 → replx-1.3}/replx/cli/agent/server/handlers/__init__.py +0 -0
  48. {replx-1.2 → replx-1.3}/replx/cli/agent/server/handlers/repl.py +0 -0
  49. {replx-1.2 → replx-1.3}/replx/cli/commands/__init__.py +0 -0
  50. {replx-1.2 → replx-1.3}/replx/cli/commands/exec.py +0 -0
  51. {replx-1.2 → replx-1.3}/replx/cli/commands/file.py +0 -0
  52. {replx-1.2 → replx-1.3}/replx/cli/commands/firmware.py +0 -0
  53. {replx-1.2 → replx-1.3}/replx/cli/commands/package.py +0 -0
  54. {replx-1.2 → replx-1.3}/replx/cli/commands/utility.py +0 -0
  55. {replx-1.2 → replx-1.3}/replx/cli/helpers/store.py +0 -0
  56. {replx-1.2 → replx-1.3}/replx/commands.py +0 -0
  57. {replx-1.2 → replx-1.3}/replx/protocol/__init__.py +0 -0
  58. {replx-1.2 → replx-1.3}/replx/protocol/repl.py +0 -0
  59. {replx-1.2 → replx-1.3}/replx/protocol/storage.py +0 -0
  60. {replx-1.2 → replx-1.3}/replx/tests/__init__.py +0 -0
  61. {replx-1.2 → replx-1.3}/replx/tests/test_compiler_arch.py +0 -0
  62. {replx-1.2 → replx-1.3}/replx/tests/test_connection_info_lookup.py +0 -0
  63. {replx-1.2 → replx-1.3}/replx/tests/test_device_info_esp_multi_core.py +0 -0
  64. {replx-1.2 → replx-1.3}/replx/tests/test_pkg_local_version.py +0 -0
  65. {replx-1.2 → replx-1.3}/replx/tests/test_session_id_fallback.py +0 -0
  66. {replx-1.2 → replx-1.3}/replx/transport/base.py +0 -0
  67. {replx-1.2 → replx-1.3}/replx/typehints/comm/_thread.pyi +0 -0
  68. {replx-1.2 → replx-1.3}/replx/typehints/comm/aioble/__init__.pyi +0 -0
  69. {replx-1.2 → replx-1.3}/replx/typehints/comm/array.pyi +0 -0
  70. {replx-1.2 → replx-1.3}/replx/typehints/comm/asyncio/__init__.pyi +0 -0
  71. {replx-1.2 → replx-1.3}/replx/typehints/comm/binascii.pyi +0 -0
  72. {replx-1.2 → replx-1.3}/replx/typehints/comm/bluetooth.pyi +0 -0
  73. {replx-1.2 → replx-1.3}/replx/typehints/comm/builtins.pyi +0 -0
  74. {replx-1.2 → replx-1.3}/replx/typehints/comm/cmath.pyi +0 -0
  75. {replx-1.2 → replx-1.3}/replx/typehints/comm/collections.pyi +0 -0
  76. {replx-1.2 → replx-1.3}/replx/typehints/comm/cryptolib.pyi +0 -0
  77. {replx-1.2 → replx-1.3}/replx/typehints/comm/deflate.pyi +0 -0
  78. {replx-1.2 → replx-1.3}/replx/typehints/comm/errno.pyi +0 -0
  79. {replx-1.2 → replx-1.3}/replx/typehints/comm/framebuf.pyi +0 -0
  80. {replx-1.2 → replx-1.3}/replx/typehints/comm/gc.pyi +0 -0
  81. {replx-1.2 → replx-1.3}/replx/typehints/comm/hashlib.pyi +0 -0
  82. {replx-1.2 → replx-1.3}/replx/typehints/comm/heapq.pyi +0 -0
  83. {replx-1.2 → replx-1.3}/replx/typehints/comm/io.pyi +0 -0
  84. {replx-1.2 → replx-1.3}/replx/typehints/comm/json.pyi +0 -0
  85. {replx-1.2 → replx-1.3}/replx/typehints/comm/lwip.pyi +0 -0
  86. {replx-1.2 → replx-1.3}/replx/typehints/comm/machine.pyi +0 -0
  87. {replx-1.2 → replx-1.3}/replx/typehints/comm/math.pyi +0 -0
  88. {replx-1.2 → replx-1.3}/replx/typehints/comm/micropython.pyi +0 -0
  89. {replx-1.2 → replx-1.3}/replx/typehints/comm/mip/__init__.pyi +0 -0
  90. {replx-1.2 → replx-1.3}/replx/typehints/comm/network.pyi +0 -0
  91. {replx-1.2 → replx-1.3}/replx/typehints/comm/ntptime.pyi +0 -0
  92. {replx-1.2 → replx-1.3}/replx/typehints/comm/os.pyi +0 -0
  93. {replx-1.2 → replx-1.3}/replx/typehints/comm/platform.pyi +0 -0
  94. {replx-1.2 → replx-1.3}/replx/typehints/comm/random.pyi +0 -0
  95. {replx-1.2 → replx-1.3}/replx/typehints/comm/re.pyi +0 -0
  96. {replx-1.2 → replx-1.3}/replx/typehints/comm/requests/__init__.pyi +0 -0
  97. {replx-1.2 → replx-1.3}/replx/typehints/comm/select.pyi +0 -0
  98. {replx-1.2 → replx-1.3}/replx/typehints/comm/socket.pyi +0 -0
  99. {replx-1.2 → replx-1.3}/replx/typehints/comm/ssl.pyi +0 -0
  100. {replx-1.2 → replx-1.3}/replx/typehints/comm/struct.pyi +0 -0
  101. {replx-1.2 → replx-1.3}/replx/typehints/comm/sys.pyi +0 -0
  102. {replx-1.2 → replx-1.3}/replx/typehints/comm/time.pyi +0 -0
  103. {replx-1.2 → replx-1.3}/replx/typehints/comm/tls.pyi +0 -0
  104. {replx-1.2 → replx-1.3}/replx/typehints/comm/uasyncio.pyi +0 -0
  105. {replx-1.2 → replx-1.3}/replx/typehints/comm/uctypes.pyi +0 -0
  106. {replx-1.2 → replx-1.3}/replx/typehints/comm/urequests.pyi +0 -0
  107. {replx-1.2 → replx-1.3}/replx/typehints/comm/vfs.pyi +0 -0
  108. {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/binascii.pyi +0 -0
  109. {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/errno.pyi +0 -0
  110. {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/hashlib.pyi +0 -0
  111. {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/io.pyi +0 -0
  112. {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/json.pyi +0 -0
  113. {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/machine.pyi +0 -0
  114. {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/math.pyi +0 -0
  115. {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/micropython.pyi +0 -0
  116. {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/network.pyi +0 -0
  117. {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/os.pyi +0 -0
  118. {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/select.pyi +0 -0
  119. {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/socket.pyi +0 -0
  120. {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/ssl.pyi +0 -0
  121. {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/struct.pyi +0 -0
  122. {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/sys.pyi +0 -0
  123. {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/time.pyi +0 -0
  124. {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/ubinascii.pyi +0 -0
  125. {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/ucryptolib.pyi +0 -0
  126. {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/uerrno.pyi +0 -0
  127. {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/uhashlib.pyi +0 -0
  128. {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/uio.pyi +0 -0
  129. {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/ujson.pyi +0 -0
  130. {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/umachine.pyi +0 -0
  131. {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/uos.pyi +0 -0
  132. {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/uselect.pyi +0 -0
  133. {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/usocket.pyi +0 -0
  134. {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/ussl.pyi +0 -0
  135. {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/ustruct.pyi +0 -0
  136. {replx-1.2 → replx-1.3}/replx/typehints/comm_separate/EFR32MG/utime.pyi +0 -0
  137. {replx-1.2 → replx-1.3}/replx/typehints/core/ESP32/aioespnow.pyi +0 -0
  138. {replx-1.2 → replx-1.3}/replx/typehints/core/ESP32/esp.pyi +0 -0
  139. {replx-1.2 → replx-1.3}/replx/typehints/core/ESP32/esp32.pyi +0 -0
  140. {replx-1.2 → replx-1.3}/replx/typehints/core/ESP32/espnow.pyi +0 -0
  141. {replx-1.2 → replx-1.3}/replx/typehints/core/MIMXRT1062DVJ6A/mimxrt.pyi +0 -0
  142. {replx-1.2 → replx-1.3}/replx/typehints/core/RP2350/rp2.pyi +0 -0
  143. {replx-1.2 → replx-1.3}/replx/utils/constants.py +0 -0
  144. {replx-1.2 → replx-1.3}/replx/utils/exceptions.py +0 -0
  145. {replx-1.2 → replx-1.3}/replx.egg-info/dependency_links.txt +0 -0
  146. {replx-1.2 → replx-1.3}/replx.egg-info/entry_points.txt +0 -0
  147. {replx-1.2 → replx-1.3}/replx.egg-info/requires.txt +0 -0
  148. {replx-1.2 → replx-1.3}/replx.egg-info/top_level.txt +0 -0
  149. {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
@@ -1,5 +1,5 @@
1
1
  __all__ = ["__version__", "get_version", "__description__"]
2
- __version__ = "1.2"
2
+ __version__ = "1.3"
3
3
  __description__ = "Fast, modern MicroPython CLI with REPL, file sync, install, and smart port detection."
4
4
  __author__ = "PlanX Lab Development Team"
5
5
 
@@ -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) # Shorter sleep for faster response
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, # Canonical key (Windows COM ports uppercased)
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
- first_port, first_conn = next(iter(all_connections.items()))
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
- # Remove trailing '>' (raw REPL prompt after execution)
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 FileNotFoundError(f"Script not found: {script_path}")
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)