replx 1.22.2__tar.gz → 1.30.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 (163) hide show
  1. {replx-1.22.2/replx.egg-info → replx-1.30.3}/PKG-INFO +13 -7
  2. {replx-1.22.2 → replx-1.30.3}/README.md +6 -1
  3. {replx-1.22.2 → replx-1.30.3}/pyproject.toml +7 -8
  4. {replx-1.22.2 → replx-1.30.3}/replx/__init__.py +2 -2
  5. {replx-1.22.2 → replx-1.30.3}/replx/cli/agent/__init__.py +10 -3
  6. {replx-1.22.2 → replx-1.30.3}/replx/cli/agent/client/core.py +39 -12
  7. {replx-1.22.2 → replx-1.30.3}/replx/cli/agent/protocol.py +9 -14
  8. {replx-1.22.2 → replx-1.30.3}/replx/cli/agent/server/connection_manager.py +105 -49
  9. {replx-1.22.2 → replx-1.30.3}/replx/cli/agent/server/core.py +130 -52
  10. {replx-1.22.2 → replx-1.30.3}/replx/cli/agent/server/handlers/exec.py +60 -30
  11. {replx-1.22.2 → replx-1.30.3}/replx/cli/agent/server/handlers/filesystem.py +2 -1
  12. {replx-1.22.2 → replx-1.30.3}/replx/cli/agent/server/handlers/repl.py +2 -0
  13. {replx-1.22.2 → replx-1.30.3}/replx/cli/agent/server/handlers/session.py +11 -17
  14. {replx-1.22.2 → replx-1.30.3}/replx/cli/agent/server/handlers/transfer.py +52 -65
  15. {replx-1.22.2 → replx-1.30.3}/replx/cli/agent/server/handlers/uart.py +4 -2
  16. {replx-1.22.2 → replx-1.30.3}/replx/cli/agent/server/session_manager.py +48 -5
  17. {replx-1.22.2 → replx-1.30.3}/replx/cli/agent/server/udp.py +20 -12
  18. {replx-1.22.2 → replx-1.30.3}/replx/cli/app.py +7 -6
  19. {replx-1.22.2 → replx-1.30.3}/replx/cli/commands/_common.py +52 -8
  20. {replx-1.22.2 → replx-1.30.3}/replx/cli/commands/adc.py +1185 -1166
  21. {replx-1.22.2 → replx-1.30.3}/replx/cli/commands/ble.py +32 -51
  22. {replx-1.22.2 → replx-1.30.3}/replx/cli/commands/device.py +54 -112
  23. {replx-1.22.2 → replx-1.30.3}/replx/cli/commands/exec.py +9 -11
  24. {replx-1.22.2 → replx-1.30.3}/replx/cli/commands/file.py +28 -28
  25. {replx-1.22.2 → replx-1.30.3}/replx/cli/commands/firmware.py +132 -6
  26. {replx-1.22.2 → replx-1.30.3}/replx/cli/commands/gpio.py +111 -27
  27. {replx-1.22.2 → replx-1.30.3}/replx/cli/commands/i2c.py +43 -34
  28. {replx-1.22.2 → replx-1.30.3}/replx/cli/commands/i2s.py +489 -462
  29. {replx-1.22.2 → replx-1.30.3}/replx/cli/commands/mip.py +11 -3
  30. {replx-1.22.2 → replx-1.30.3}/replx/cli/commands/package.py +65 -4
  31. {replx-1.22.2 → replx-1.30.3}/replx/cli/commands/pwm.py +1010 -1007
  32. {replx-1.22.2 → replx-1.30.3}/replx/cli/commands/spi.py +718 -714
  33. {replx-1.22.2 → replx-1.30.3}/replx/cli/commands/uart.py +388 -121
  34. {replx-1.22.2 → replx-1.30.3}/replx/cli/commands/utility.py +45 -5
  35. {replx-1.22.2 → replx-1.30.3}/replx/cli/commands/wifi.py +88 -79
  36. {replx-1.22.2 → replx-1.30.3}/replx/cli/commands/ws2812.py +237 -222
  37. {replx-1.22.2 → replx-1.30.3}/replx/cli/config.py +0 -24
  38. {replx-1.22.2 → replx-1.30.3}/replx/cli/connection.py +1 -15
  39. {replx-1.22.2 → replx-1.30.3}/replx/cli/helpers/compiler.py +10 -2
  40. replx-1.30.3/replx/cli/helpers/scanner.py +308 -0
  41. {replx-1.22.2 → replx-1.30.3}/replx/commands.py +3 -2
  42. {replx-1.22.2 → replx-1.30.3}/replx/protocol/repl.py +155 -110
  43. {replx-1.22.2 → replx-1.30.3}/replx/protocol/storage.py +182 -128
  44. {replx-1.22.2 → replx-1.30.3}/replx/terminal.py +72 -10
  45. replx-1.30.3/replx/transport/__init__.py +32 -0
  46. {replx-1.22.2 → replx-1.30.3}/replx/transport/serial.py +55 -23
  47. {replx-1.22.2 → replx-1.30.3}/replx/utils/constants.py +14 -0
  48. {replx-1.22.2 → replx-1.30.3}/replx/utils/device_info.py +13 -3
  49. {replx-1.22.2 → replx-1.30.3/replx.egg-info}/PKG-INFO +13 -7
  50. replx-1.30.3/replx.egg-info/requires.txt +7 -0
  51. replx-1.22.2/replx/cli/helpers/scanner.py +0 -256
  52. replx-1.22.2/replx/transport/__init__.py +0 -15
  53. replx-1.22.2/replx.egg-info/requires.txt +0 -6
  54. {replx-1.22.2 → replx-1.30.3}/LICENSE +0 -0
  55. {replx-1.22.2 → replx-1.30.3}/replx/__main__.py +0 -0
  56. {replx-1.22.2 → replx-1.30.3}/replx/cli/__init__.py +0 -0
  57. {replx-1.22.2 → replx-1.30.3}/replx/cli/agent/client/__init__.py +0 -0
  58. {replx-1.22.2 → replx-1.30.3}/replx/cli/agent/client/session.py +0 -0
  59. {replx-1.22.2 → replx-1.30.3}/replx/cli/agent/server/__init__.py +0 -0
  60. {replx-1.22.2 → replx-1.30.3}/replx/cli/agent/server/__main__.py +0 -0
  61. {replx-1.22.2 → replx-1.30.3}/replx/cli/agent/server/command_dispatcher.py +0 -0
  62. {replx-1.22.2 → replx-1.30.3}/replx/cli/agent/server/handlers/__init__.py +0 -0
  63. {replx-1.22.2 → replx-1.30.3}/replx/cli/agent/server/handlers/i2c.py +0 -0
  64. {replx-1.22.2 → replx-1.30.3}/replx/cli/agent/server/handlers/spi.py +0 -0
  65. {replx-1.22.2 → replx-1.30.3}/replx/cli/commands/__init__.py +0 -0
  66. {replx-1.22.2 → replx-1.30.3}/replx/cli/helpers/__init__.py +0 -0
  67. {replx-1.22.2 → replx-1.30.3}/replx/cli/helpers/environment.py +0 -0
  68. {replx-1.22.2 → replx-1.30.3}/replx/cli/helpers/output.py +0 -0
  69. {replx-1.22.2 → replx-1.30.3}/replx/cli/helpers/registry.py +0 -0
  70. {replx-1.22.2 → replx-1.30.3}/replx/cli/helpers/store.py +0 -0
  71. {replx-1.22.2 → replx-1.30.3}/replx/cli/helpers/updater.py +0 -0
  72. {replx-1.22.2 → replx-1.30.3}/replx/protocol/__init__.py +0 -0
  73. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm/_thread.pyi +0 -0
  74. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm/aioble/__init__.pyi +0 -0
  75. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm/aioble/l2cap.pyi +0 -0
  76. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm/array.pyi +0 -0
  77. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm/asyncio/__init__.pyi +0 -0
  78. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm/binascii.pyi +0 -0
  79. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm/bluetooth.pyi +0 -0
  80. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm/builtins.pyi +0 -0
  81. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm/cmath.pyi +0 -0
  82. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm/collections.pyi +0 -0
  83. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm/cryptolib.pyi +0 -0
  84. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm/deflate.pyi +0 -0
  85. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm/errno.pyi +0 -0
  86. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm/framebuf.pyi +0 -0
  87. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm/gc.pyi +0 -0
  88. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm/hashlib.pyi +0 -0
  89. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm/heapq.pyi +0 -0
  90. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm/io.pyi +0 -0
  91. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm/json.pyi +0 -0
  92. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm/lwip.pyi +0 -0
  93. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm/machine.pyi +0 -0
  94. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm/math.pyi +0 -0
  95. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm/micropython.pyi +0 -0
  96. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm/mip/__init__.pyi +0 -0
  97. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm/network.pyi +0 -0
  98. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm/ntptime.pyi +0 -0
  99. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm/os.pyi +0 -0
  100. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm/platform.pyi +0 -0
  101. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm/random.pyi +0 -0
  102. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm/re.pyi +0 -0
  103. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm/requests/__init__.pyi +0 -0
  104. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm/select.pyi +0 -0
  105. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm/socket.pyi +0 -0
  106. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm/ssl.pyi +0 -0
  107. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm/struct.pyi +0 -0
  108. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm/sys.pyi +0 -0
  109. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm/time.pyi +0 -0
  110. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm/tls.pyi +0 -0
  111. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm/uasyncio.pyi +0 -0
  112. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm/uctypes.pyi +0 -0
  113. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm/ulab.pyi +0 -0
  114. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm/urequests.pyi +0 -0
  115. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm/vfs.pyi +0 -0
  116. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm_separate/EFR32MG/binascii.pyi +0 -0
  117. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm_separate/EFR32MG/digi/__init__.pyi +0 -0
  118. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm_separate/EFR32MG/digi/ble.pyi +0 -0
  119. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm_separate/EFR32MG/errno.pyi +0 -0
  120. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm_separate/EFR32MG/hashlib.pyi +0 -0
  121. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm_separate/EFR32MG/io.pyi +0 -0
  122. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm_separate/EFR32MG/json.pyi +0 -0
  123. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm_separate/EFR32MG/machine.pyi +0 -0
  124. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm_separate/EFR32MG/math.pyi +0 -0
  125. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm_separate/EFR32MG/micropython.pyi +0 -0
  126. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm_separate/EFR32MG/network.pyi +0 -0
  127. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm_separate/EFR32MG/os.pyi +0 -0
  128. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm_separate/EFR32MG/select.pyi +0 -0
  129. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm_separate/EFR32MG/socket.pyi +0 -0
  130. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm_separate/EFR32MG/ssl.pyi +0 -0
  131. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm_separate/EFR32MG/struct.pyi +0 -0
  132. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm_separate/EFR32MG/sys.pyi +0 -0
  133. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm_separate/EFR32MG/time.pyi +0 -0
  134. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm_separate/EFR32MG/ubinascii.pyi +0 -0
  135. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm_separate/EFR32MG/ucryptolib.pyi +0 -0
  136. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm_separate/EFR32MG/uerrno.pyi +0 -0
  137. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm_separate/EFR32MG/uhashlib.pyi +0 -0
  138. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm_separate/EFR32MG/uio.pyi +0 -0
  139. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm_separate/EFR32MG/ujson.pyi +0 -0
  140. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm_separate/EFR32MG/umachine.pyi +0 -0
  141. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm_separate/EFR32MG/uos.pyi +0 -0
  142. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm_separate/EFR32MG/uselect.pyi +0 -0
  143. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm_separate/EFR32MG/usocket.pyi +0 -0
  144. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm_separate/EFR32MG/ussl.pyi +0 -0
  145. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm_separate/EFR32MG/ustruct.pyi +0 -0
  146. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm_separate/EFR32MG/utime.pyi +0 -0
  147. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm_separate/EFR32MG/xbee/__init__.pyi +0 -0
  148. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm_separate/EFR32MG/xbee/modem_status.pyi +0 -0
  149. {replx-1.22.2 → replx-1.30.3}/replx/typehints/comm_separate/EFR32MG/xbee/relay.pyi +0 -0
  150. {replx-1.22.2 → replx-1.30.3}/replx/typehints/core/ESP32/aioespnow.pyi +0 -0
  151. {replx-1.22.2 → replx-1.30.3}/replx/typehints/core/ESP32/esp.pyi +0 -0
  152. {replx-1.22.2 → replx-1.30.3}/replx/typehints/core/ESP32/esp32.pyi +0 -0
  153. {replx-1.22.2 → replx-1.30.3}/replx/typehints/core/ESP32/espnow.pyi +0 -0
  154. {replx-1.22.2 → replx-1.30.3}/replx/typehints/core/MIMXRT1062DVJ6A/mimxrt.pyi +0 -0
  155. {replx-1.22.2 → replx-1.30.3}/replx/typehints/core/RP2350/rp2.pyi +0 -0
  156. {replx-1.22.2 → replx-1.30.3}/replx/utils/__init__.py +0 -0
  157. {replx-1.22.2 → replx-1.30.3}/replx/utils/exceptions.py +0 -0
  158. {replx-1.22.2 → replx-1.30.3}/replx.egg-info/SOURCES.txt +0 -0
  159. {replx-1.22.2 → replx-1.30.3}/replx.egg-info/dependency_links.txt +0 -0
  160. {replx-1.22.2 → replx-1.30.3}/replx.egg-info/entry_points.txt +0 -0
  161. {replx-1.22.2 → replx-1.30.3}/replx.egg-info/top_level.txt +0 -0
  162. {replx-1.22.2 → replx-1.30.3}/setup.cfg +0 -0
  163. {replx-1.22.2 → replx-1.30.3}/test/test_hardware_commands.py +0 -0
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: replx
3
- Version: 1.22.2
3
+ Version: 1.30.3
4
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
5
  Author-email: "chanmin.park" <devcamp@planxlab.com>
6
6
  License-Expression: MIT
7
- Project-URL: Homepage, https://github.com/PlanXLab/replx
7
+ Project-URL: Homepage, https://planxlab.com
8
8
  Project-URL: Repository, https://github.com/PlanXLab/replx
9
9
  Project-URL: Issues, https://github.com/PlanXLab/replx/issues
10
10
  Keywords: micropython,repl,serial,pyserial,typer,mpy-cross,deploy,gpio,i2c,spi,uart,pwm,adc,i2s,ws2812
@@ -17,12 +17,13 @@ Classifier: Topic :: System :: Hardware :: Universal Serial Bus (USB)
17
17
  Requires-Python: >=3.10
18
18
  Description-Content-Type: text/markdown
19
19
  License-File: LICENSE
20
- Requires-Dist: click>=8.4.2
21
- Requires-Dist: typer>=0.27.0
22
- Requires-Dist: rich>=13.0
20
+ Requires-Dist: click>=8.5.0
21
+ Requires-Dist: typer>=0.27.3
22
+ Requires-Dist: rich>=15.0.0
23
23
  Requires-Dist: pyserial>=3.5
24
- Requires-Dist: mpy-cross>=1.27.0
24
+ Requires-Dist: mpy-cross>=1.29.0
25
25
  Requires-Dist: psutil>=7.2.2
26
+ Requires-Dist: esptool<6,>=5.6.0
26
27
  Dynamic: license-file
27
28
 
28
29
  # replx
@@ -33,6 +34,8 @@ Dynamic: license-file
33
34
 
34
35
  `replx` is a CLI tool for MicroPython development. It uses a single local agent process to manage multiple CLI sessions and multiple boards in a consistent workflow.
35
36
 
37
+ It runs on Windows, macOS, and Linux and targets standard MicroPython 1.29 or newer, with a separate source-based legacy compatibility path for Digi XBee3 Zigbee. Hardware commands are platform-dependent; see the detailed manual's board compatibility table. Actual board testing has been completed on Raspberry Pi Pico 2 W and Waveshare ESP32-P4-WIFI6; Digi XBee3 compatibility still requires physical board validation.
38
+
36
39
  ## What replx provides
37
40
 
38
41
  - Shared connection management across terminal sessions
@@ -59,6 +62,7 @@ pip install replx
59
62
  - `whoami`: Show the current foreground device.
60
63
  - `disconnect`: Close a device connection.
61
64
  - `shutdown`: Stop the agent and clear active sessions.
65
+ - `theme`: Show or customize UI panel colors and box style.
62
66
 
63
67
  ### Execution and interaction
64
68
 
@@ -77,7 +81,7 @@ pip install replx
77
81
  - `mv`: Move or rename files or directories on device.
78
82
  - `rm`: Remove files or directories on device.
79
83
  - `mkdir`: Create directories on device.
80
- - `touch`: Create an empty file or update timestamps.
84
+ - `touch`: Create a file if missing; preserve existing contents.
81
85
 
82
86
  ### Device management
83
87
 
@@ -101,6 +105,8 @@ pip install replx
101
105
  - `uart`: Open, write, read, and monitor UART.
102
106
  - `spi`: Open, write, read, and transfer SPI data.
103
107
  - `i2c`: Scan, read, write, and dump I2C devices.
108
+ - `i2s`: Play I2S tones and capture or monitor an I2S microphone.
109
+ - `ws2812`: Write pixels and regions on WS2812 LED matrices.
104
110
 
105
111
  ### Connectivity
106
112
 
@@ -6,6 +6,8 @@
6
6
 
7
7
  `replx` is a CLI tool for MicroPython development. It uses a single local agent process to manage multiple CLI sessions and multiple boards in a consistent workflow.
8
8
 
9
+ It runs on Windows, macOS, and Linux and targets standard MicroPython 1.29 or newer, with a separate source-based legacy compatibility path for Digi XBee3 Zigbee. Hardware commands are platform-dependent; see the detailed manual's board compatibility table. Actual board testing has been completed on Raspberry Pi Pico 2 W and Waveshare ESP32-P4-WIFI6; Digi XBee3 compatibility still requires physical board validation.
10
+
9
11
  ## What replx provides
10
12
 
11
13
  - Shared connection management across terminal sessions
@@ -32,6 +34,7 @@ pip install replx
32
34
  - `whoami`: Show the current foreground device.
33
35
  - `disconnect`: Close a device connection.
34
36
  - `shutdown`: Stop the agent and clear active sessions.
37
+ - `theme`: Show or customize UI panel colors and box style.
35
38
 
36
39
  ### Execution and interaction
37
40
 
@@ -50,7 +53,7 @@ pip install replx
50
53
  - `mv`: Move or rename files or directories on device.
51
54
  - `rm`: Remove files or directories on device.
52
55
  - `mkdir`: Create directories on device.
53
- - `touch`: Create an empty file or update timestamps.
56
+ - `touch`: Create a file if missing; preserve existing contents.
54
57
 
55
58
  ### Device management
56
59
 
@@ -74,6 +77,8 @@ pip install replx
74
77
  - `uart`: Open, write, read, and monitor UART.
75
78
  - `spi`: Open, write, read, and transfer SPI data.
76
79
  - `i2c`: Scan, read, write, and dump I2C devices.
80
+ - `i2s`: Play I2S tones and capture or monitor an I2S microphone.
81
+ - `ws2812`: Write pixels and regions on WS2812 LED matrices.
77
82
 
78
83
  ### Connectivity
79
84
 
@@ -1,5 +1,5 @@
1
1
  [build-system]
2
- requires = ["setuptools>=70", "wheel"]
2
+ requires = ["setuptools>=77.0.3"]
3
3
  build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
@@ -23,18 +23,17 @@ classifiers = [
23
23
  ]
24
24
 
25
25
  dependencies = [
26
- "click>=8.4.2",
27
- "typer>=0.27.0",
28
- "rich>=13.0",
26
+ "click>=8.5.0",
27
+ "typer>=0.27.3",
28
+ "rich>=15.0.0",
29
29
  "pyserial>=3.5",
30
- "mpy-cross>=1.27.0",
30
+ "mpy-cross>=1.29.0",
31
31
  "psutil>=7.2.2",
32
+ "esptool>=5.6.0,<6",
32
33
  ]
33
34
 
34
- [project.optional-dependencies]
35
-
36
35
  [project.urls]
37
- Homepage = "https://github.com/PlanXLab/replx"
36
+ Homepage = "https://planxlab.com"
38
37
  Repository = "https://github.com/PlanXLab/replx"
39
38
  Issues = "https://github.com/PlanXLab/replx/issues"
40
39
 
@@ -1,5 +1,5 @@
1
1
  __all__ = ["__version__", "get_version", "__description__"]
2
- __version__ = "1.22.2"
2
+ __version__ = "1.30.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
 
@@ -8,4 +8,4 @@ def get_version() -> str:
8
8
  from importlib.metadata import version as _v # Python 3.10+
9
9
  return _v("replx")
10
10
  except Exception:
11
- return __version__
11
+ return __version__
@@ -1,6 +1,7 @@
1
1
 
2
+ from importlib import import_module
3
+
2
4
  from .protocol import AgentProtocol
3
- from .client import AgentClient, get_session_id, get_cached_session_id, clear_session_cache
4
5
 
5
6
  __all__ = [
6
7
  'AgentProtocol',
@@ -14,8 +15,14 @@ __all__ = [
14
15
 
15
16
 
16
17
  def __getattr__(name):
17
- # The server stack pulls in ``replx.terminal`` (~150ms) plus all command
18
- # handlers; CLI clients never need it. Defer until something actually asks.
18
+ if name in {
19
+ 'AgentClient',
20
+ 'get_session_id',
21
+ 'get_cached_session_id',
22
+ 'clear_session_cache',
23
+ }:
24
+ return getattr(import_module('.client', __name__), name)
25
+
19
26
  if name == 'AgentServer':
20
27
  from .server import AgentServer
21
28
 
@@ -26,14 +26,14 @@ class AgentClient:
26
26
 
27
27
  self._ppid = get_cached_session_id()
28
28
  self._atexit_registered = False
29
+ # Set only when Ctrl+C interrupts a request the agent may still be
30
+ # executing. A normal CLI exit must keep the agent's board connection.
31
+ self._release_on_exit = False
29
32
 
30
33
  def connect(self):
31
34
  if not self.sock:
32
35
  self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
33
36
  self.sock.settimeout(self.TIMEOUT)
34
- # M7: ensure the agent releases the device port if the CLI process
35
- # exits abnormally (Ctrl-C, unhandled exception, parent terminated)
36
- # without its normal teardown path running.
37
37
  if not self._atexit_registered and self.device_port:
38
38
  try:
39
39
  atexit.register(self._atexit_release)
@@ -43,9 +43,8 @@ class AgentClient:
43
43
 
44
44
  def _atexit_release(self) -> None:
45
45
  try:
46
- if self.sock is None:
47
- return
48
- self._release_device_port_safe()
46
+ self._release_on_exit = False
47
+ self.disconnect()
49
48
  except Exception:
50
49
  pass
51
50
 
@@ -92,8 +91,22 @@ class AgentClient:
92
91
  except Exception:
93
92
  pass
94
93
 
94
+ @staticmethod
95
+ def _print_notifications(response: Dict[str, Any]) -> None:
96
+ for notification in response.get('notifications', ()):
97
+ if notification:
98
+ print(f"replx: {notification}", file=sys.stderr)
99
+
95
100
  def send_command(self, command: str, timeout: float = None, max_retries: int = None,
96
101
  wait_on_busy: float = 30.0, **args) -> Dict[str, Any]:
102
+ try:
103
+ return self._send_command(command, timeout, max_retries, wait_on_busy, **args)
104
+ except KeyboardInterrupt:
105
+ self._release_on_exit = True
106
+ raise
107
+
108
+ def _send_command(self, command: str, timeout: float = None, max_retries: int = None,
109
+ wait_on_busy: float = 30.0, **args) -> Dict[str, Any]:
97
110
  if not self.sock:
98
111
  self.connect()
99
112
 
@@ -161,6 +174,7 @@ class AgentClient:
161
174
  if not response:
162
175
  raise RuntimeError("No response from agent")
163
176
 
177
+ self._print_notifications(response)
164
178
  error = response.get('error', '')
165
179
 
166
180
  # Retry only for transient "Another command" busy — not REPL/detached.
@@ -178,7 +192,8 @@ class AgentClient:
178
192
  output_callback: Callable[[bytes, str], None] = None,
179
193
  input_provider: Callable[[], Optional[bytes]] = None,
180
194
  stop_check: Callable[[], bool] = None,
181
- ctrl_c_grace_s: float = 3.0) -> Dict[str, Any]:
195
+ ctrl_c_grace_s: float = 3.0,
196
+ reset: bool = True) -> Dict[str, Any]:
182
197
  if not self.sock:
183
198
  self.connect()
184
199
 
@@ -191,7 +206,8 @@ class AgentClient:
191
206
  port=self.device_port,
192
207
  script_path=script_path,
193
208
  script_content=script_content,
194
- echo=echo
209
+ echo=echo,
210
+ reset=reset,
195
211
  )
196
212
  seq = request['seq']
197
213
  request_data = AgentProtocol.encode_message(request)
@@ -324,9 +340,10 @@ class AgentClient:
324
340
 
325
341
  # Check for stream reception timeout (connection loss detection)
326
342
  if now - last_stream_time > stream_timeout:
327
- # Release the device port on the agent so subsequent
328
- # commands don't operate on a stale serial connection.
329
- self._release_device_port_safe()
343
+ try:
344
+ self.send_command(Cmd.RUN_STOP, timeout=1.0)
345
+ except Exception:
346
+ pass
330
347
  raise RuntimeError("Connection lost - no data from board for {}s".format(stream_timeout))
331
348
 
332
349
  if now - last_input_time >= input_interval:
@@ -394,6 +411,15 @@ class AgentClient:
394
411
  def send_command_streaming(self, command: str, timeout: float = None,
395
412
  progress_callback: Callable[[Dict[str, Any]], None] = None,
396
413
  **args) -> Dict[str, Any]:
414
+ try:
415
+ return self._send_command_streaming(command, timeout, progress_callback, **args)
416
+ except KeyboardInterrupt:
417
+ self._release_on_exit = True
418
+ raise
419
+
420
+ def _send_command_streaming(self, command: str, timeout: float = None,
421
+ progress_callback: Callable[[Dict[str, Any]], None] = None,
422
+ **args) -> Dict[str, Any]:
397
423
  if not self.sock:
398
424
  self.connect()
399
425
 
@@ -458,6 +484,7 @@ class AgentClient:
458
484
  if not response:
459
485
  raise RuntimeError("No response from agent (timeout)")
460
486
 
487
+ self._print_notifications(response)
461
488
  if response.get('error'):
462
489
  raise RuntimeError(response['error'])
463
490
 
@@ -519,7 +546,7 @@ class AgentClient:
519
546
 
520
547
  proc = subprocess.Popen(
521
548
  cmd,
522
- creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS,
549
+ creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.CREATE_NO_WINDOW,
523
550
  stdout=subprocess.DEVNULL,
524
551
  stderr=stderr_file if stderr_file else subprocess.DEVNULL,
525
552
  startupinfo=startupinfo
@@ -9,6 +9,7 @@ from replx.utils.constants import MAX_PAYLOAD_SIZE
9
9
 
10
10
  _MAGIC = b'RPLX'
11
11
  _VERSION = 1
12
+ _HEADER = struct.Struct('!4sBI')
12
13
 
13
14
  _seq_lock = threading.Lock()
14
15
  _seq_counter = int(time.time() * 1000000) & 0xFFFFFFFE # even start
@@ -24,38 +25,32 @@ def _next_seq() -> int:
24
25
  class AgentProtocol:
25
26
  @staticmethod
26
27
  def encode_message(msg: Dict[str, Any]) -> bytes:
27
- payload = json.dumps(msg).encode('utf-8')
28
+ payload = json.dumps(msg, separators=(',', ':')).encode('utf-8')
28
29
 
29
30
  if len(payload) > MAX_PAYLOAD_SIZE:
30
31
  raise ValueError(f"Message too large: {len(payload)} bytes")
31
32
 
32
- header = (
33
- _MAGIC +
34
- struct.pack('!B', _VERSION) +
35
- struct.pack('!I', len(payload))
36
- )
33
+ header = _HEADER.pack(_MAGIC, _VERSION, len(payload))
37
34
 
38
35
  return header + payload
39
36
 
40
37
  @staticmethod
41
38
  def decode_message(data: bytes) -> Optional[Dict[str, Any]]:
42
- if len(data) < 9:
39
+ if len(data) < _HEADER.size:
43
40
  return None
44
41
 
45
- if data[:4] != _MAGIC:
42
+ magic, version, length = _HEADER.unpack_from(data)
43
+ if magic != _MAGIC:
46
44
  return None
47
45
 
48
- version = struct.unpack('!B', data[4:5])[0]
49
46
  if version != _VERSION:
50
47
  return None
51
48
 
52
- length = struct.unpack('!I', data[5:9])[0]
53
-
54
- if len(data) < 9 + length:
49
+ payload_end = _HEADER.size + length
50
+ if len(data) < payload_end:
55
51
  return None
56
52
 
57
- payload = data[9:9+length]
58
- return json.loads(payload.decode('utf-8'))
53
+ return json.loads(data[_HEADER.size:payload_end])
59
54
 
60
55
  @staticmethod
61
56
  def create_request(command: str, seq: int = None, ppid: int = None, port: str = None, **args) -> Dict[str, Any]:
@@ -2,14 +2,18 @@ import re
2
2
  import sys
3
3
  import time
4
4
  import threading
5
+ from collections import deque
5
6
  from concurrent.futures import Future as ConcurrentFuture
6
7
  from dataclasses import dataclass, field
7
- from typing import Optional, Dict, Any, List, Tuple
8
+ from typing import Optional, Dict, Any, Deque, List, Tuple
8
9
 
9
10
  from replx.protocol import ReplProtocol, create_storage
10
11
  from replx.utils import parse_device_banner, canon_port as _shared_canon_port
11
- from replx.utils.constants import CTRL_B, CTRL_C
12
+ from replx.utils.constants import (
13
+ CTRL_B, CTRL_C, CONNECT_DETECT_ROUNDS, CONNECT_DETECT_RETRY_DELAY,
14
+ )
12
15
  from replx.utils.exceptions import TransportError
16
+ from replx.utils.device_info import get_root_fs_for_core
13
17
 
14
18
 
15
19
  def _detect_device_info(transport, core: str, device: str = None) -> Tuple[str, str, str, str]:
@@ -58,7 +62,7 @@ class InteractiveSessionState:
58
62
  seq: int = 0
59
63
  client_addr: Optional[tuple] = None
60
64
  echo: bool = True
61
- input_queue: List[Any] = field(default_factory=list)
65
+ input_queue: Deque[bytes] = field(default_factory=deque)
62
66
  stop_requested: bool = False
63
67
  thread: Optional[threading.Thread] = None
64
68
  error: Optional[str] = None
@@ -71,7 +75,7 @@ class InteractiveSessionState:
71
75
  self.seq = seq
72
76
  self.client_addr = client_addr
73
77
  self.echo = echo
74
- self.input_queue = []
78
+ self.input_queue.clear()
75
79
  self.stop_requested = False
76
80
  self.error = None
77
81
 
@@ -82,7 +86,7 @@ class InteractiveSessionState:
82
86
  self.ppid = None
83
87
  self.seq = 0
84
88
  self.client_addr = None
85
- self.input_queue = []
89
+ self.input_queue.clear()
86
90
  self.stop_requested = False
87
91
  thread_to_join = self.thread
88
92
  self.thread = None
@@ -104,14 +108,14 @@ class ReplSessionState:
104
108
  ppid: Optional[int] = None
105
109
  reader_future: Optional[ConcurrentFuture] = None
106
110
  reader_thread: Optional[threading.Thread] = None
107
- output_buffer: bytes = b""
111
+ output_buffer: bytearray = field(default_factory=bytearray)
108
112
  buffer_lock: threading.Lock = field(default_factory=threading.Lock)
109
113
 
110
114
  def start(self, ppid: int):
111
115
  self.active = True
112
116
  self.ppid = ppid
113
117
  with self.buffer_lock:
114
- self.output_buffer = b""
118
+ self.output_buffer.clear()
115
119
 
116
120
  def stop(self):
117
121
  self.active = False
@@ -123,7 +127,7 @@ class ReplSessionState:
123
127
  self.reader_thread.join(timeout=1)
124
128
  self.reader_thread = None
125
129
  with self.buffer_lock:
126
- self.output_buffer = b""
130
+ self.output_buffer.clear()
127
131
 
128
132
  def is_owner(self, ppid: int) -> bool:
129
133
  if ppid is None:
@@ -134,13 +138,13 @@ class ReplSessionState:
134
138
 
135
139
  def append_output(self, data: bytes):
136
140
  with self.buffer_lock:
137
- self.output_buffer += data
141
+ self.output_buffer.extend(data)
138
142
 
139
143
  def read_output(self) -> bytes:
140
144
  with self.buffer_lock:
141
145
  data = self.output_buffer
142
- self.output_buffer = b""
143
- return data
146
+ self.output_buffer = bytearray()
147
+ return bytes(data)
144
148
 
145
149
  @dataclass
146
150
  class BoardConnection:
@@ -236,12 +240,26 @@ class BoardConnection:
236
240
  self.last_command_time = time.time()
237
241
  return True
238
242
 
239
- def release(self):
243
+ def acquire_for_heartbeat(self) -> bool:
244
+ with self._busy_lock:
245
+ if self.busy or self.detached_running or self.repl.active or self.interactive.active:
246
+ return False
247
+ self.busy = True
248
+ self.busy_session = self.HEARTBEAT_SESSION_ID
249
+ self.busy_command = 'heartbeat'
250
+ self.busy_client = None
251
+ return True
252
+
253
+ def release(self, *, reset_liveness: bool = True, update_last_command_time: bool = True):
240
254
  with self._busy_lock:
241
255
  self.busy = False
242
256
  self.busy_session = None
243
257
  self.busy_command = None
244
258
  self.busy_client = None
259
+ if update_last_command_time:
260
+ self.last_command_time = time.time()
261
+ if reset_liveness:
262
+ self._liveness_fail_count = 0
245
263
 
246
264
  def set_busy_state(self, busy: bool, command: str = None, session: int = None,
247
265
  client_addr: tuple = None) -> None:
@@ -292,6 +310,7 @@ class ConnectionManager:
292
310
  # Per-port events used to serialize concurrent create_serial_connection
293
311
  # calls for the same port (prevents double serial-open race).
294
312
  self._port_creating: Dict[str, threading.Event] = {}
313
+ self._scan_lock = threading.Lock()
295
314
  self._default_port: Optional[str] = None
296
315
 
297
316
  @property
@@ -355,54 +374,89 @@ class ConnectionManager:
355
374
  with self._connections_lock:
356
375
  return len(self._connections)
357
376
 
377
+ @property
378
+ def scan_lock(self) -> threading.Lock:
379
+ return self._scan_lock
380
+
381
+ def reserve_probe_port(self, port: str) -> Optional[Tuple[str, threading.Event]]:
382
+ port_key = self._canon_port(port)
383
+ if not port_key:
384
+ return None
385
+ with self._connections_lock:
386
+ if port_key in self._connections or port_key in self._port_creating:
387
+ return None
388
+ event = threading.Event()
389
+ self._port_creating[port_key] = event
390
+ return port_key, event
391
+
392
+ def release_probe_port(self, reservation: Tuple[str, threading.Event]) -> None:
393
+ port_key, event = reservation
394
+ with self._connections_lock:
395
+ if self._port_creating.get(port_key) is event:
396
+ self._port_creating.pop(port_key, None)
397
+ event.set()
398
+
358
399
  def create_serial_connection(
359
400
  self,
360
401
  port: str,
361
402
  core: str = "RP2350",
362
403
  device: str = None,
363
- baudrate: int = 115200
404
+ baudrate: Optional[int] = None
364
405
  ) -> Tuple[BoardConnection, Optional[str]]:
365
406
  original_port = str(port).strip() if port is not None else ""
366
407
  port_key = self._canon_port(original_port)
367
408
 
368
- # --- Phase 1: fast check + race serialisation ---
369
- with self._connections_lock:
370
- if port_key in self._connections:
371
- conn = self._connections[port_key]
372
- if conn.is_connected():
373
- return conn, None
374
- # If another thread is already opening this port, get its event so
375
- # we can wait for it instead of opening the same serial port twice.
376
- if port_key in self._port_creating:
377
- wait_event = self._port_creating[port_key]
378
- our_event = None
379
- else:
380
- our_event = threading.Event()
381
- self._port_creating[port_key] = our_event
382
- wait_event = None
383
-
384
- if wait_event is not None:
385
- # Another thread is opening this port; wait then return its result.
386
- wait_event.wait(timeout=30)
409
+ # --- Phase 1: serialize opens and scan probes for this port ---
410
+ deadline = time.monotonic() + 30.0
411
+ while True:
387
412
  with self._connections_lock:
388
413
  conn = self._connections.get(port_key)
389
414
  if conn and conn.is_connected():
390
415
  return conn, None
391
- return None, "Connection creation timed out (concurrent attempt)"
416
+ wait_event = self._port_creating.get(port_key)
417
+ if wait_event is None:
418
+ our_event = threading.Event()
419
+ self._port_creating[port_key] = our_event
420
+ break
421
+
422
+ remaining = deadline - time.monotonic()
423
+ if remaining <= 0 or not wait_event.wait(timeout=remaining):
424
+ return None, "Connection creation timed out (concurrent attempt)"
392
425
 
393
426
  # --- Phase 2: we are the creator ---
394
427
  try:
395
- from replx.transport import create_transport
396
-
397
- transport = create_transport(f"serial:{original_port}", baudrate=baudrate)
428
+ from replx.transport import create_transport, repl_baud_candidates
429
+
430
+ rates = [baudrate] if baudrate else repl_baud_candidates()
431
+ transport = create_transport(f"serial:{original_port}", baudrate=rates[0])
432
+
433
+ # A board that was just reset (e.g. by esptool or a USB-UART bridge's
434
+ # DTR/RTS lines) may still be booting, so a failed sweep is retried.
435
+ version = "?"
436
+ current_rate = rates[0]
437
+ for attempt in range(CONNECT_DETECT_ROUNDS):
438
+ if attempt:
439
+ time.sleep(CONNECT_DETECT_RETRY_DELAY)
440
+ for rate in rates:
441
+ if rate != current_rate:
442
+ transport.set_baudrate(rate)
443
+ current_rate = rate
444
+ version, detected_core, detected_device, manufacturer = _detect_device_info(
445
+ transport, core, device
446
+ )
447
+ if version != "?":
448
+ break
449
+ if version != "?":
450
+ break
451
+ else:
452
+ # No rate produced a recognisable REPL; keep the primary rate as before.
453
+ if current_rate != rates[0]:
454
+ transport.set_baudrate(rates[0])
398
455
 
399
- version, detected_core, detected_device, manufacturer = _detect_device_info(
400
- transport, core, device
456
+ device_root_fs = get_root_fs_for_core(detected_core)
457
+ repl_protocol = ReplProtocol(
458
+ transport, core=detected_core, device_root_fs=device_root_fs
401
459
  )
402
-
403
- repl_protocol = ReplProtocol(transport)
404
-
405
- device_root_fs = "/"
406
460
  if version != "?":
407
461
  try:
408
462
  result = repl_protocol.exec("import os; print(os.getcwd())")
@@ -412,14 +466,16 @@ class ConnectionManager:
412
466
  cwd = result.strip() if result else "/"
413
467
  else:
414
468
  cwd = "/"
415
- if cwd.startswith('/flash'):
469
+ if cwd == '/flash' or cwd.startswith('/flash/'):
416
470
  device_root_fs = '/flash/'
417
- elif cwd.startswith('/'):
418
- parts = cwd.split('/')
419
- if len(parts) > 1 and parts[1]:
420
- device_root_fs = f'/{parts[1]}/'
421
- except Exception:
422
- pass
471
+ except Exception as exc:
472
+ print(
473
+ f"Could not query filesystem root on {original_port}; "
474
+ f"using {device_root_fs} from the {detected_core} profile: {exc}",
475
+ file=sys.stderr,
476
+ )
477
+
478
+ repl_protocol.device_root_fs = device_root_fs
423
479
 
424
480
  file_system = create_storage(
425
481
  repl_protocol,