replx 1.3__tar.gz → 1.5__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 (155) hide show
  1. replx-1.5/PKG-INFO +101 -0
  2. replx-1.5/README.md +75 -0
  3. {replx-1.3 → replx-1.5}/replx/__init__.py +1 -1
  4. {replx-1.3 → replx-1.5}/replx/cli/agent/client/core.py +20 -12
  5. {replx-1.3 → replx-1.5}/replx/cli/agent/server/connection_manager.py +17 -7
  6. {replx-1.3 → replx-1.5}/replx/cli/agent/server/core.py +249 -132
  7. {replx-1.3 → replx-1.5}/replx/cli/agent/server/handlers/exec.py +9 -8
  8. {replx-1.3 → replx-1.5}/replx/cli/agent/server/handlers/repl.py +53 -7
  9. {replx-1.3 → replx-1.5}/replx/cli/agent/server/handlers/session.py +6 -14
  10. {replx-1.3 → replx-1.5}/replx/cli/app.py +1 -1
  11. {replx-1.3 → replx-1.5}/replx/cli/commands/device.py +55 -20
  12. {replx-1.3 → replx-1.5}/replx/cli/commands/exec.py +42 -12
  13. {replx-1.3 → replx-1.5}/replx/cli/commands/package.py +124 -163
  14. {replx-1.3 → replx-1.5}/replx/cli/commands/utility.py +13 -13
  15. {replx-1.3 → replx-1.5}/replx/cli/helpers/registry.py +25 -7
  16. {replx-1.3 → replx-1.5}/replx/commands.py +3 -3
  17. {replx-1.3 → replx-1.5}/replx/protocol/repl.py +46 -5
  18. {replx-1.3 → replx-1.5}/replx/terminal.py +46 -0
  19. replx-1.5/replx/tests/test_agent_asyncio.py +319 -0
  20. replx-1.5/replx/tests/test_agent_port_canonicalization.py +43 -0
  21. replx-1.5/replx/tests/test_agent_thread_pool.py +216 -0
  22. replx-1.5/replx/tests/test_disconnect_cleanup.py +57 -0
  23. replx-1.5/replx/tests/test_lock_cleanup.py +194 -0
  24. replx-1.5/replx/tests/test_pkg_search_scope_filter.py +82 -0
  25. replx-1.5/replx/tests/test_repl_reader_task.py +313 -0
  26. replx-1.5/replx/tests/test_session_disconnect_releases_shared_port.py +47 -0
  27. replx-1.5/replx/tests/test_shutdown_status_message.py +56 -0
  28. replx-1.5/replx/tests/test_windows_com_port_normalization.py +69 -0
  29. {replx-1.3 → replx-1.5}/replx/transport/serial.py +13 -8
  30. replx-1.5/replx.egg-info/PKG-INFO +101 -0
  31. {replx-1.3 → replx-1.5}/replx.egg-info/SOURCES.txt +13 -1
  32. replx-1.5/test/test_termio.py +42 -0
  33. replx-1.3/PKG-INFO +0 -25
  34. replx-1.3/replx.egg-info/PKG-INFO +0 -25
  35. {replx-1.3 → replx-1.5}/LICENSE +0 -0
  36. {replx-1.3 → replx-1.5}/pyproject.toml +0 -0
  37. {replx-1.3 → replx-1.5}/replx/cli/__init__.py +0 -0
  38. {replx-1.3 → replx-1.5}/replx/cli/agent/__init__.py +0 -0
  39. {replx-1.3 → replx-1.5}/replx/cli/agent/client/__init__.py +0 -0
  40. {replx-1.3 → replx-1.5}/replx/cli/agent/client/session.py +0 -0
  41. {replx-1.3 → replx-1.5}/replx/cli/agent/protocol.py +0 -0
  42. {replx-1.3 → replx-1.5}/replx/cli/agent/server/__init__.py +0 -0
  43. {replx-1.3 → replx-1.5}/replx/cli/agent/server/__main__.py +0 -0
  44. {replx-1.3 → replx-1.5}/replx/cli/agent/server/command_dispatcher.py +0 -0
  45. {replx-1.3 → replx-1.5}/replx/cli/agent/server/handlers/__init__.py +0 -0
  46. {replx-1.3 → replx-1.5}/replx/cli/agent/server/handlers/filesystem.py +0 -0
  47. {replx-1.3 → replx-1.5}/replx/cli/agent/server/handlers/transfer.py +0 -0
  48. {replx-1.3 → replx-1.5}/replx/cli/agent/server/session_manager.py +0 -0
  49. {replx-1.3 → replx-1.5}/replx/cli/commands/__init__.py +0 -0
  50. {replx-1.3 → replx-1.5}/replx/cli/commands/file.py +0 -0
  51. {replx-1.3 → replx-1.5}/replx/cli/commands/firmware.py +0 -0
  52. {replx-1.3 → replx-1.5}/replx/cli/config.py +0 -0
  53. {replx-1.3 → replx-1.5}/replx/cli/connection.py +0 -0
  54. {replx-1.3 → replx-1.5}/replx/cli/helpers/__init__.py +0 -0
  55. {replx-1.3 → replx-1.5}/replx/cli/helpers/compiler.py +0 -0
  56. {replx-1.3 → replx-1.5}/replx/cli/helpers/environment.py +0 -0
  57. {replx-1.3 → replx-1.5}/replx/cli/helpers/output.py +0 -0
  58. {replx-1.3 → replx-1.5}/replx/cli/helpers/scanner.py +0 -0
  59. {replx-1.3 → replx-1.5}/replx/cli/helpers/store.py +0 -0
  60. {replx-1.3 → replx-1.5}/replx/cli/helpers/updater.py +0 -0
  61. {replx-1.3 → replx-1.5}/replx/protocol/__init__.py +0 -0
  62. {replx-1.3 → replx-1.5}/replx/protocol/storage.py +0 -0
  63. {replx-1.3 → replx-1.5}/replx/tests/__init__.py +0 -0
  64. {replx-1.3 → replx-1.5}/replx/tests/test_compiler_arch.py +0 -0
  65. {replx-1.3 → replx-1.5}/replx/tests/test_connection_info_lookup.py +0 -0
  66. {replx-1.3 → replx-1.5}/replx/tests/test_device_info_esp_multi_core.py +0 -0
  67. {replx-1.3 → replx-1.5}/replx/tests/test_pkg_local_version.py +0 -0
  68. {replx-1.3 → replx-1.5}/replx/tests/test_session_id_fallback.py +0 -0
  69. {replx-1.3 → replx-1.5}/replx/transport/__init__.py +0 -0
  70. {replx-1.3 → replx-1.5}/replx/transport/base.py +0 -0
  71. {replx-1.3 → replx-1.5}/replx/typehints/comm/_thread.pyi +0 -0
  72. {replx-1.3 → replx-1.5}/replx/typehints/comm/aioble/__init__.pyi +0 -0
  73. {replx-1.3 → replx-1.5}/replx/typehints/comm/array.pyi +0 -0
  74. {replx-1.3 → replx-1.5}/replx/typehints/comm/asyncio/__init__.pyi +0 -0
  75. {replx-1.3 → replx-1.5}/replx/typehints/comm/binascii.pyi +0 -0
  76. {replx-1.3 → replx-1.5}/replx/typehints/comm/bluetooth.pyi +0 -0
  77. {replx-1.3 → replx-1.5}/replx/typehints/comm/builtins.pyi +0 -0
  78. {replx-1.3 → replx-1.5}/replx/typehints/comm/cmath.pyi +0 -0
  79. {replx-1.3 → replx-1.5}/replx/typehints/comm/collections.pyi +0 -0
  80. {replx-1.3 → replx-1.5}/replx/typehints/comm/cryptolib.pyi +0 -0
  81. {replx-1.3 → replx-1.5}/replx/typehints/comm/deflate.pyi +0 -0
  82. {replx-1.3 → replx-1.5}/replx/typehints/comm/errno.pyi +0 -0
  83. {replx-1.3 → replx-1.5}/replx/typehints/comm/framebuf.pyi +0 -0
  84. {replx-1.3 → replx-1.5}/replx/typehints/comm/gc.pyi +0 -0
  85. {replx-1.3 → replx-1.5}/replx/typehints/comm/hashlib.pyi +0 -0
  86. {replx-1.3 → replx-1.5}/replx/typehints/comm/heapq.pyi +0 -0
  87. {replx-1.3 → replx-1.5}/replx/typehints/comm/io.pyi +0 -0
  88. {replx-1.3 → replx-1.5}/replx/typehints/comm/json.pyi +0 -0
  89. {replx-1.3 → replx-1.5}/replx/typehints/comm/lwip.pyi +0 -0
  90. {replx-1.3 → replx-1.5}/replx/typehints/comm/machine.pyi +0 -0
  91. {replx-1.3 → replx-1.5}/replx/typehints/comm/math.pyi +0 -0
  92. {replx-1.3 → replx-1.5}/replx/typehints/comm/micropython.pyi +0 -0
  93. {replx-1.3 → replx-1.5}/replx/typehints/comm/mip/__init__.pyi +0 -0
  94. {replx-1.3 → replx-1.5}/replx/typehints/comm/network.pyi +0 -0
  95. {replx-1.3 → replx-1.5}/replx/typehints/comm/ntptime.pyi +0 -0
  96. {replx-1.3 → replx-1.5}/replx/typehints/comm/os.pyi +0 -0
  97. {replx-1.3 → replx-1.5}/replx/typehints/comm/platform.pyi +0 -0
  98. {replx-1.3 → replx-1.5}/replx/typehints/comm/random.pyi +0 -0
  99. {replx-1.3 → replx-1.5}/replx/typehints/comm/re.pyi +0 -0
  100. {replx-1.3 → replx-1.5}/replx/typehints/comm/requests/__init__.pyi +0 -0
  101. {replx-1.3 → replx-1.5}/replx/typehints/comm/select.pyi +0 -0
  102. {replx-1.3 → replx-1.5}/replx/typehints/comm/socket.pyi +0 -0
  103. {replx-1.3 → replx-1.5}/replx/typehints/comm/ssl.pyi +0 -0
  104. {replx-1.3 → replx-1.5}/replx/typehints/comm/struct.pyi +0 -0
  105. {replx-1.3 → replx-1.5}/replx/typehints/comm/sys.pyi +0 -0
  106. {replx-1.3 → replx-1.5}/replx/typehints/comm/time.pyi +0 -0
  107. {replx-1.3 → replx-1.5}/replx/typehints/comm/tls.pyi +0 -0
  108. {replx-1.3 → replx-1.5}/replx/typehints/comm/uasyncio.pyi +0 -0
  109. {replx-1.3 → replx-1.5}/replx/typehints/comm/uctypes.pyi +0 -0
  110. {replx-1.3 → replx-1.5}/replx/typehints/comm/urequests.pyi +0 -0
  111. {replx-1.3 → replx-1.5}/replx/typehints/comm/vfs.pyi +0 -0
  112. {replx-1.3 → replx-1.5}/replx/typehints/comm_separate/EFR32MG/binascii.pyi +0 -0
  113. {replx-1.3 → replx-1.5}/replx/typehints/comm_separate/EFR32MG/errno.pyi +0 -0
  114. {replx-1.3 → replx-1.5}/replx/typehints/comm_separate/EFR32MG/hashlib.pyi +0 -0
  115. {replx-1.3 → replx-1.5}/replx/typehints/comm_separate/EFR32MG/io.pyi +0 -0
  116. {replx-1.3 → replx-1.5}/replx/typehints/comm_separate/EFR32MG/json.pyi +0 -0
  117. {replx-1.3 → replx-1.5}/replx/typehints/comm_separate/EFR32MG/machine.pyi +0 -0
  118. {replx-1.3 → replx-1.5}/replx/typehints/comm_separate/EFR32MG/math.pyi +0 -0
  119. {replx-1.3 → replx-1.5}/replx/typehints/comm_separate/EFR32MG/micropython.pyi +0 -0
  120. {replx-1.3 → replx-1.5}/replx/typehints/comm_separate/EFR32MG/network.pyi +0 -0
  121. {replx-1.3 → replx-1.5}/replx/typehints/comm_separate/EFR32MG/os.pyi +0 -0
  122. {replx-1.3 → replx-1.5}/replx/typehints/comm_separate/EFR32MG/select.pyi +0 -0
  123. {replx-1.3 → replx-1.5}/replx/typehints/comm_separate/EFR32MG/socket.pyi +0 -0
  124. {replx-1.3 → replx-1.5}/replx/typehints/comm_separate/EFR32MG/ssl.pyi +0 -0
  125. {replx-1.3 → replx-1.5}/replx/typehints/comm_separate/EFR32MG/struct.pyi +0 -0
  126. {replx-1.3 → replx-1.5}/replx/typehints/comm_separate/EFR32MG/sys.pyi +0 -0
  127. {replx-1.3 → replx-1.5}/replx/typehints/comm_separate/EFR32MG/time.pyi +0 -0
  128. {replx-1.3 → replx-1.5}/replx/typehints/comm_separate/EFR32MG/ubinascii.pyi +0 -0
  129. {replx-1.3 → replx-1.5}/replx/typehints/comm_separate/EFR32MG/ucryptolib.pyi +0 -0
  130. {replx-1.3 → replx-1.5}/replx/typehints/comm_separate/EFR32MG/uerrno.pyi +0 -0
  131. {replx-1.3 → replx-1.5}/replx/typehints/comm_separate/EFR32MG/uhashlib.pyi +0 -0
  132. {replx-1.3 → replx-1.5}/replx/typehints/comm_separate/EFR32MG/uio.pyi +0 -0
  133. {replx-1.3 → replx-1.5}/replx/typehints/comm_separate/EFR32MG/ujson.pyi +0 -0
  134. {replx-1.3 → replx-1.5}/replx/typehints/comm_separate/EFR32MG/umachine.pyi +0 -0
  135. {replx-1.3 → replx-1.5}/replx/typehints/comm_separate/EFR32MG/uos.pyi +0 -0
  136. {replx-1.3 → replx-1.5}/replx/typehints/comm_separate/EFR32MG/uselect.pyi +0 -0
  137. {replx-1.3 → replx-1.5}/replx/typehints/comm_separate/EFR32MG/usocket.pyi +0 -0
  138. {replx-1.3 → replx-1.5}/replx/typehints/comm_separate/EFR32MG/ussl.pyi +0 -0
  139. {replx-1.3 → replx-1.5}/replx/typehints/comm_separate/EFR32MG/ustruct.pyi +0 -0
  140. {replx-1.3 → replx-1.5}/replx/typehints/comm_separate/EFR32MG/utime.pyi +0 -0
  141. {replx-1.3 → replx-1.5}/replx/typehints/core/ESP32/aioespnow.pyi +0 -0
  142. {replx-1.3 → replx-1.5}/replx/typehints/core/ESP32/esp.pyi +0 -0
  143. {replx-1.3 → replx-1.5}/replx/typehints/core/ESP32/esp32.pyi +0 -0
  144. {replx-1.3 → replx-1.5}/replx/typehints/core/ESP32/espnow.pyi +0 -0
  145. {replx-1.3 → replx-1.5}/replx/typehints/core/MIMXRT1062DVJ6A/mimxrt.pyi +0 -0
  146. {replx-1.3 → replx-1.5}/replx/typehints/core/RP2350/rp2.pyi +0 -0
  147. {replx-1.3 → replx-1.5}/replx/utils/__init__.py +0 -0
  148. {replx-1.3 → replx-1.5}/replx/utils/constants.py +0 -0
  149. {replx-1.3 → replx-1.5}/replx/utils/device_info.py +0 -0
  150. {replx-1.3 → replx-1.5}/replx/utils/exceptions.py +0 -0
  151. {replx-1.3 → replx-1.5}/replx.egg-info/dependency_links.txt +0 -0
  152. {replx-1.3 → replx-1.5}/replx.egg-info/entry_points.txt +0 -0
  153. {replx-1.3 → replx-1.5}/replx.egg-info/requires.txt +0 -0
  154. {replx-1.3 → replx-1.5}/replx.egg-info/top_level.txt +0 -0
  155. {replx-1.3 → replx-1.5}/setup.cfg +0 -0
replx-1.5/PKG-INFO ADDED
@@ -0,0 +1,101 @@
1
+ Metadata-Version: 2.4
2
+ Name: replx
3
+ Version: 1.5
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
26
+
27
+ # replx
28
+
29
+ [![PyPI version](https://badge.fury.io/py/replx.svg)](https://badge.fury.io/py/replx)
30
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
31
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
32
+
33
+ `replx` is a CLI tool for MicroPython development. It uses an agent-based architecture to manage multiple CLI sessions and multiple boards in a consistent workflow.
34
+
35
+ ## What replx provides
36
+
37
+ - Shared connection management across terminal sessions
38
+ - Foreground and background board handling per session
39
+ - Workspace-level default device configuration
40
+ - File operations on device storage
41
+ - Script execution, REPL access, and utility commands
42
+
43
+ ## Installation
44
+
45
+ ```sh
46
+ pip install replx
47
+ ```
48
+
49
+ ## Command summary
50
+
51
+ ### Connection and session
52
+
53
+ - `setup`: Initialize workspace settings and register a default device.
54
+ - `scan`: List available serial devices.
55
+ - `status`: Show session and connection state.
56
+ - `fg`: Change the foreground device for the current session.
57
+ - `whoami`: Show the current foreground device.
58
+ - `disconnect`: Close a device connection.
59
+ - `shutdown`: Stop the agent and clear active sessions.
60
+
61
+ ### Execution and interaction
62
+
63
+ - `exec` (`-c`): Execute inline Python code on the device.
64
+ - `run`: Run a local or device-side script.
65
+ - `repl`: Open an interactive REPL session.
66
+ - `shell`: Open a device file-system shell.
67
+
68
+ ### File operations
69
+
70
+ - `ls`: List files and directories.
71
+ - `cat`: Print file content.
72
+ - `get`: Download files from device to local.
73
+ - `put`: Upload files from local to device.
74
+ - `cp`: Copy files or directories on device.
75
+ - `mv`: Move or rename files or directories on device.
76
+ - `rm`: Remove files or directories on device.
77
+ - `mkdir`: Create directories on device.
78
+ - `touch`: Create an empty file or update timestamps.
79
+
80
+ ### Device management
81
+
82
+ - `usage`: Show device storage usage.
83
+ - `reset`: Perform a soft reset.
84
+ - `format`: Format the device file system.
85
+ - `init`: Run initialization scripts on device.
86
+ - `wifi`: Manage Wi-Fi configuration and status.
87
+ - `firmware`: Check, download, or update firmware.
88
+
89
+ ### Package and build
90
+
91
+ - `pkg`: Search, download, and update packages.
92
+ - `mpy`: Compile `.py` files to `.mpy`.
93
+
94
+ ## Notes
95
+
96
+ - `scan`, `status`, `whoami`, and `shutdown` are special commands and do not accept `--port`.
97
+ - Most device commands can omit the port when a foreground or workspace default device is available.
98
+
99
+ ## License
100
+
101
+ MIT
replx-1.5/README.md ADDED
@@ -0,0 +1,75 @@
1
+ # replx
2
+
3
+ [![PyPI version](https://badge.fury.io/py/replx.svg)](https://badge.fury.io/py/replx)
4
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ `replx` is a CLI tool for MicroPython development. It uses an agent-based architecture to manage multiple CLI sessions and multiple boards in a consistent workflow.
8
+
9
+ ## What replx provides
10
+
11
+ - Shared connection management across terminal sessions
12
+ - Foreground and background board handling per session
13
+ - Workspace-level default device configuration
14
+ - File operations on device storage
15
+ - Script execution, REPL access, and utility commands
16
+
17
+ ## Installation
18
+
19
+ ```sh
20
+ pip install replx
21
+ ```
22
+
23
+ ## Command summary
24
+
25
+ ### Connection and session
26
+
27
+ - `setup`: Initialize workspace settings and register a default device.
28
+ - `scan`: List available serial devices.
29
+ - `status`: Show session and connection state.
30
+ - `fg`: Change the foreground device for the current session.
31
+ - `whoami`: Show the current foreground device.
32
+ - `disconnect`: Close a device connection.
33
+ - `shutdown`: Stop the agent and clear active sessions.
34
+
35
+ ### Execution and interaction
36
+
37
+ - `exec` (`-c`): Execute inline Python code on the device.
38
+ - `run`: Run a local or device-side script.
39
+ - `repl`: Open an interactive REPL session.
40
+ - `shell`: Open a device file-system shell.
41
+
42
+ ### File operations
43
+
44
+ - `ls`: List files and directories.
45
+ - `cat`: Print file content.
46
+ - `get`: Download files from device to local.
47
+ - `put`: Upload files from local to device.
48
+ - `cp`: Copy files or directories on device.
49
+ - `mv`: Move or rename files or directories on device.
50
+ - `rm`: Remove files or directories on device.
51
+ - `mkdir`: Create directories on device.
52
+ - `touch`: Create an empty file or update timestamps.
53
+
54
+ ### Device management
55
+
56
+ - `usage`: Show device storage usage.
57
+ - `reset`: Perform a soft reset.
58
+ - `format`: Format the device file system.
59
+ - `init`: Run initialization scripts on device.
60
+ - `wifi`: Manage Wi-Fi configuration and status.
61
+ - `firmware`: Check, download, or update firmware.
62
+
63
+ ### Package and build
64
+
65
+ - `pkg`: Search, download, and update packages.
66
+ - `mpy`: Compile `.py` files to `.mpy`.
67
+
68
+ ## Notes
69
+
70
+ - `scan`, `status`, `whoami`, and `shutdown` are special commands and do not accept `--port`.
71
+ - Most device commands can omit the port when a foreground or workspace default device is available.
72
+
73
+ ## License
74
+
75
+ MIT
@@ -1,5 +1,5 @@
1
1
  __all__ = ["__version__", "get_version", "__description__"]
2
- __version__ = "1.3"
2
+ __version__ = "1.5"
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
 
@@ -33,7 +33,7 @@ class AgentClient:
33
33
  self.sock.close()
34
34
  self.sock = None
35
35
 
36
- def send_command(self, command: str, timeout: float = None, **args) -> Dict[str, Any]:
36
+ def send_command(self, command: str, timeout: float = None, max_retries: int = None, **args) -> Dict[str, Any]:
37
37
  if not self.sock:
38
38
  self.connect()
39
39
 
@@ -58,8 +58,11 @@ class AgentClient:
58
58
  request_data = AgentProtocol.encode_message(request)
59
59
 
60
60
  response = None
61
-
62
- max_attempts = 1 if effective_timeout < 1.0 else self.MAX_RETRIES
61
+
62
+ if max_retries is not None:
63
+ max_attempts = max(1, max_retries)
64
+ else:
65
+ max_attempts = 1 if effective_timeout < 1.0 else self.MAX_RETRIES
63
66
 
64
67
  for attempt in range(max_attempts):
65
68
  try:
@@ -154,7 +157,6 @@ class AgentClient:
154
157
  self.sock.settimeout(0.01)
155
158
  input_interval = 0.001
156
159
  last_input_time = 0
157
- error_check_until = time.time() + 0.1
158
160
 
159
161
  try:
160
162
  while True:
@@ -179,15 +181,15 @@ class AgentClient:
179
181
  except Exception:
180
182
  pass
181
183
 
184
+ _pending_error = None
182
185
  try:
183
186
  data, addr = self.sock.recvfrom(MAX_UDP_SIZE)
184
187
  msg = AgentProtocol.decode_message(data)
185
188
 
186
189
  if msg and msg.get('seq') == seq:
187
- if now < error_check_until and msg.get('type') == 'response' and msg.get('error'):
188
- raise RuntimeError(msg['error'])
189
-
190
- if msg.get('type') == 'stream':
190
+ if msg.get('type') == 'response' and msg.get('error'):
191
+ _pending_error = msg['error']
192
+ elif msg.get('type') == 'stream':
191
193
  output = msg.get('output', '')
192
194
  if output and output_callback:
193
195
  output_callback(output.encode('utf-8'), 'stdout')
@@ -203,6 +205,9 @@ class AgentClient:
203
205
  except Exception:
204
206
  pass
205
207
 
208
+ if _pending_error:
209
+ raise RuntimeError(_pending_error)
210
+
206
211
  except KeyboardInterrupt:
207
212
  try:
208
213
  self.send_command(Cmd.RUN_STOP, timeout=0.5)
@@ -321,13 +326,14 @@ class AgentClient:
321
326
  if port:
322
327
  cmd.append(str(port))
323
328
 
329
+ proc = None
324
330
  if background:
325
331
  if sys.platform == 'win32':
326
332
  startupinfo = subprocess.STARTUPINFO()
327
333
  startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
328
334
  startupinfo.wShowWindow = 0
329
335
 
330
- subprocess.Popen(
336
+ proc = subprocess.Popen(
331
337
  cmd,
332
338
  creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS,
333
339
  stdout=subprocess.DEVNULL,
@@ -335,7 +341,7 @@ class AgentClient:
335
341
  startupinfo=startupinfo
336
342
  )
337
343
  else:
338
- subprocess.Popen(
344
+ proc = subprocess.Popen(
339
345
  cmd,
340
346
  stdout=subprocess.DEVNULL,
341
347
  stderr=subprocess.DEVNULL,
@@ -344,10 +350,12 @@ class AgentClient:
344
350
  close_fds=True
345
351
  )
346
352
  else:
347
- subprocess.Popen(cmd)
353
+ proc = subprocess.Popen(cmd)
348
354
 
349
- for i in range(30):
355
+ for _ in range(100):
350
356
  time.sleep(0.1)
357
+ if proc is not None and proc.poll() is not None:
358
+ raise RuntimeError(f"Failed to start agent (process exited with code {proc.returncode})")
351
359
  if AgentClient.is_agent_running(port=port):
352
360
  return True
353
361
 
@@ -2,11 +2,13 @@ import re
2
2
  import sys
3
3
  import time
4
4
  import threading
5
+ from concurrent.futures import Future as ConcurrentFuture
5
6
  from dataclasses import dataclass, field
6
7
  from typing import Optional, Dict, Any, List, Tuple
7
8
 
8
9
  from replx.protocol import ReplProtocol, create_storage
9
10
  from replx.utils import parse_device_banner
11
+ from replx.utils.constants import CTRL_B, CTRL_C
10
12
  from replx.utils.exceptions import TransportError
11
13
 
12
14
 
@@ -16,11 +18,11 @@ def _detect_device_info(transport, core: str, device: str = None) -> Tuple[str,
16
18
  delay3 = 0.1 if sys.platform != "win32" else 0.2
17
19
 
18
20
  try:
19
- transport.write(b'\r\x03')
21
+ transport.write(b'\r' + CTRL_C)
20
22
  time.sleep(delay1)
21
23
  transport.reset_input_buffer()
22
24
 
23
- transport.write(b'\r\x02')
25
+ transport.write(b'\r' + CTRL_B)
24
26
  time.sleep(delay2)
25
27
 
26
28
  res = transport.read_available()
@@ -104,6 +106,7 @@ class InteractiveSessionState:
104
106
  class ReplSessionState:
105
107
  active: bool = False
106
108
  ppid: Optional[int] = None
109
+ reader_future: Optional[ConcurrentFuture] = None
107
110
  reader_thread: Optional[threading.Thread] = None
108
111
  output_buffer: bytes = b""
109
112
  buffer_lock: threading.Lock = field(default_factory=threading.Lock)
@@ -117,6 +120,9 @@ class ReplSessionState:
117
120
  def stop(self):
118
121
  self.active = False
119
122
  self.ppid = None
123
+ if self.reader_future is not None:
124
+ self.reader_future.cancel()
125
+ self.reader_future = None
120
126
  if self.reader_thread:
121
127
  self.reader_thread.join(timeout=1)
122
128
  self.reader_thread = None
@@ -152,7 +158,6 @@ class BoardConnection:
152
158
  version: str = "?"
153
159
  device_root_fs: str = "/"
154
160
 
155
- # Unique board ID from machine.unique_id().hex() - lazy evaluated on first command
156
161
  board_id: Optional[str] = None
157
162
 
158
163
  busy: bool = False
@@ -428,10 +433,10 @@ class ConnectionManager:
428
433
  conn = self.get_connection(port)
429
434
  if conn and conn.repl_protocol:
430
435
  try:
431
- transport = conn.repl_protocol._transport
432
- transport.write(b'\x03')
436
+ transport = conn.repl_protocol.transport
437
+ transport.write(CTRL_C)
433
438
  time.sleep(0.05)
434
- transport.write(b'\x02')
439
+ transport.write(CTRL_B)
435
440
  time.sleep(0.1)
436
441
  transport.reset_input_buffer()
437
442
  except Exception:
@@ -461,8 +466,13 @@ class ConnectionManager:
461
466
 
462
467
  if conn.repl_protocol:
463
468
  try:
464
- transport = conn.repl_protocol._transport
469
+ transport = conn.repl_protocol.transport
465
470
  if transport:
471
+ try:
472
+ transport.write(CTRL_B)
473
+ time.sleep(0.1 if sys.platform == 'win32' else 0.05)
474
+ except Exception:
475
+ pass
466
476
  transport.close()
467
477
  except Exception:
468
478
  pass