replx 1.22.2__tar.gz → 1.25.0__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.

Potentially problematic release.


This version of replx might be problematic. Click here for more details.

Files changed (163) hide show
  1. replx-1.25.0/PKG-INFO +73 -0
  2. replx-1.25.0/README.md +46 -0
  3. {replx-1.22.2 → replx-1.25.0}/pyproject.toml +1 -1
  4. {replx-1.22.2 → replx-1.25.0}/replx/__init__.py +1 -1
  5. {replx-1.22.2 → replx-1.25.0}/replx/cli/agent/__init__.py +10 -3
  6. {replx-1.22.2 → replx-1.25.0}/replx/cli/agent/protocol.py +9 -14
  7. {replx-1.22.2 → replx-1.25.0}/replx/cli/agent/server/connection_manager.py +11 -10
  8. {replx-1.22.2 → replx-1.25.0}/replx/cli/agent/server/core.py +6 -2
  9. {replx-1.22.2 → replx-1.25.0}/replx/cli/agent/server/handlers/exec.py +1 -1
  10. {replx-1.22.2 → replx-1.25.0}/replx/cli/agent/server/udp.py +20 -12
  11. {replx-1.22.2 → replx-1.25.0}/replx/cli/commands/exec.py +1 -1
  12. {replx-1.22.2 → replx-1.25.0}/replx/protocol/repl.py +45 -46
  13. {replx-1.22.2 → replx-1.25.0}/replx/protocol/storage.py +2 -1
  14. {replx-1.22.2 → replx-1.25.0}/replx/terminal.py +1 -2
  15. replx-1.25.0/replx.egg-info/PKG-INFO +73 -0
  16. replx-1.22.2/PKG-INFO +0 -118
  17. replx-1.22.2/README.md +0 -91
  18. replx-1.22.2/replx.egg-info/PKG-INFO +0 -118
  19. {replx-1.22.2 → replx-1.25.0}/LICENSE +0 -0
  20. {replx-1.22.2 → replx-1.25.0}/replx/__main__.py +0 -0
  21. {replx-1.22.2 → replx-1.25.0}/replx/cli/__init__.py +0 -0
  22. {replx-1.22.2 → replx-1.25.0}/replx/cli/agent/client/__init__.py +0 -0
  23. {replx-1.22.2 → replx-1.25.0}/replx/cli/agent/client/core.py +0 -0
  24. {replx-1.22.2 → replx-1.25.0}/replx/cli/agent/client/session.py +0 -0
  25. {replx-1.22.2 → replx-1.25.0}/replx/cli/agent/server/__init__.py +0 -0
  26. {replx-1.22.2 → replx-1.25.0}/replx/cli/agent/server/__main__.py +0 -0
  27. {replx-1.22.2 → replx-1.25.0}/replx/cli/agent/server/command_dispatcher.py +0 -0
  28. {replx-1.22.2 → replx-1.25.0}/replx/cli/agent/server/handlers/__init__.py +0 -0
  29. {replx-1.22.2 → replx-1.25.0}/replx/cli/agent/server/handlers/filesystem.py +0 -0
  30. {replx-1.22.2 → replx-1.25.0}/replx/cli/agent/server/handlers/i2c.py +0 -0
  31. {replx-1.22.2 → replx-1.25.0}/replx/cli/agent/server/handlers/repl.py +0 -0
  32. {replx-1.22.2 → replx-1.25.0}/replx/cli/agent/server/handlers/session.py +0 -0
  33. {replx-1.22.2 → replx-1.25.0}/replx/cli/agent/server/handlers/spi.py +0 -0
  34. {replx-1.22.2 → replx-1.25.0}/replx/cli/agent/server/handlers/transfer.py +0 -0
  35. {replx-1.22.2 → replx-1.25.0}/replx/cli/agent/server/handlers/uart.py +0 -0
  36. {replx-1.22.2 → replx-1.25.0}/replx/cli/agent/server/session_manager.py +0 -0
  37. {replx-1.22.2 → replx-1.25.0}/replx/cli/app.py +0 -0
  38. {replx-1.22.2 → replx-1.25.0}/replx/cli/commands/__init__.py +0 -0
  39. {replx-1.22.2 → replx-1.25.0}/replx/cli/commands/_common.py +0 -0
  40. {replx-1.22.2 → replx-1.25.0}/replx/cli/commands/adc.py +0 -0
  41. {replx-1.22.2 → replx-1.25.0}/replx/cli/commands/ble.py +0 -0
  42. {replx-1.22.2 → replx-1.25.0}/replx/cli/commands/device.py +0 -0
  43. {replx-1.22.2 → replx-1.25.0}/replx/cli/commands/file.py +0 -0
  44. {replx-1.22.2 → replx-1.25.0}/replx/cli/commands/firmware.py +0 -0
  45. {replx-1.22.2 → replx-1.25.0}/replx/cli/commands/gpio.py +0 -0
  46. {replx-1.22.2 → replx-1.25.0}/replx/cli/commands/i2c.py +0 -0
  47. {replx-1.22.2 → replx-1.25.0}/replx/cli/commands/i2s.py +0 -0
  48. {replx-1.22.2 → replx-1.25.0}/replx/cli/commands/mip.py +0 -0
  49. {replx-1.22.2 → replx-1.25.0}/replx/cli/commands/package.py +0 -0
  50. {replx-1.22.2 → replx-1.25.0}/replx/cli/commands/pwm.py +0 -0
  51. {replx-1.22.2 → replx-1.25.0}/replx/cli/commands/spi.py +0 -0
  52. {replx-1.22.2 → replx-1.25.0}/replx/cli/commands/uart.py +0 -0
  53. {replx-1.22.2 → replx-1.25.0}/replx/cli/commands/utility.py +0 -0
  54. {replx-1.22.2 → replx-1.25.0}/replx/cli/commands/wifi.py +0 -0
  55. {replx-1.22.2 → replx-1.25.0}/replx/cli/commands/ws2812.py +0 -0
  56. {replx-1.22.2 → replx-1.25.0}/replx/cli/config.py +0 -0
  57. {replx-1.22.2 → replx-1.25.0}/replx/cli/connection.py +0 -0
  58. {replx-1.22.2 → replx-1.25.0}/replx/cli/helpers/__init__.py +0 -0
  59. {replx-1.22.2 → replx-1.25.0}/replx/cli/helpers/compiler.py +0 -0
  60. {replx-1.22.2 → replx-1.25.0}/replx/cli/helpers/environment.py +0 -0
  61. {replx-1.22.2 → replx-1.25.0}/replx/cli/helpers/output.py +0 -0
  62. {replx-1.22.2 → replx-1.25.0}/replx/cli/helpers/registry.py +0 -0
  63. {replx-1.22.2 → replx-1.25.0}/replx/cli/helpers/scanner.py +0 -0
  64. {replx-1.22.2 → replx-1.25.0}/replx/cli/helpers/store.py +0 -0
  65. {replx-1.22.2 → replx-1.25.0}/replx/cli/helpers/updater.py +0 -0
  66. {replx-1.22.2 → replx-1.25.0}/replx/commands.py +0 -0
  67. {replx-1.22.2 → replx-1.25.0}/replx/protocol/__init__.py +0 -0
  68. {replx-1.22.2 → replx-1.25.0}/replx/transport/__init__.py +0 -0
  69. {replx-1.22.2 → replx-1.25.0}/replx/transport/serial.py +0 -0
  70. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm/_thread.pyi +0 -0
  71. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm/aioble/__init__.pyi +0 -0
  72. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm/aioble/l2cap.pyi +0 -0
  73. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm/array.pyi +0 -0
  74. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm/asyncio/__init__.pyi +0 -0
  75. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm/binascii.pyi +0 -0
  76. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm/bluetooth.pyi +0 -0
  77. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm/builtins.pyi +0 -0
  78. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm/cmath.pyi +0 -0
  79. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm/collections.pyi +0 -0
  80. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm/cryptolib.pyi +0 -0
  81. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm/deflate.pyi +0 -0
  82. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm/errno.pyi +0 -0
  83. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm/framebuf.pyi +0 -0
  84. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm/gc.pyi +0 -0
  85. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm/hashlib.pyi +0 -0
  86. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm/heapq.pyi +0 -0
  87. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm/io.pyi +0 -0
  88. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm/json.pyi +0 -0
  89. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm/lwip.pyi +0 -0
  90. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm/machine.pyi +0 -0
  91. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm/math.pyi +0 -0
  92. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm/micropython.pyi +0 -0
  93. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm/mip/__init__.pyi +0 -0
  94. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm/network.pyi +0 -0
  95. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm/ntptime.pyi +0 -0
  96. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm/os.pyi +0 -0
  97. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm/platform.pyi +0 -0
  98. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm/random.pyi +0 -0
  99. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm/re.pyi +0 -0
  100. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm/requests/__init__.pyi +0 -0
  101. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm/select.pyi +0 -0
  102. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm/socket.pyi +0 -0
  103. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm/ssl.pyi +0 -0
  104. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm/struct.pyi +0 -0
  105. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm/sys.pyi +0 -0
  106. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm/time.pyi +0 -0
  107. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm/tls.pyi +0 -0
  108. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm/uasyncio.pyi +0 -0
  109. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm/uctypes.pyi +0 -0
  110. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm/ulab.pyi +0 -0
  111. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm/urequests.pyi +0 -0
  112. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm/vfs.pyi +0 -0
  113. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm_separate/EFR32MG/binascii.pyi +0 -0
  114. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm_separate/EFR32MG/digi/__init__.pyi +0 -0
  115. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm_separate/EFR32MG/digi/ble.pyi +0 -0
  116. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm_separate/EFR32MG/errno.pyi +0 -0
  117. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm_separate/EFR32MG/hashlib.pyi +0 -0
  118. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm_separate/EFR32MG/io.pyi +0 -0
  119. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm_separate/EFR32MG/json.pyi +0 -0
  120. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm_separate/EFR32MG/machine.pyi +0 -0
  121. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm_separate/EFR32MG/math.pyi +0 -0
  122. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm_separate/EFR32MG/micropython.pyi +0 -0
  123. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm_separate/EFR32MG/network.pyi +0 -0
  124. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm_separate/EFR32MG/os.pyi +0 -0
  125. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm_separate/EFR32MG/select.pyi +0 -0
  126. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm_separate/EFR32MG/socket.pyi +0 -0
  127. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm_separate/EFR32MG/ssl.pyi +0 -0
  128. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm_separate/EFR32MG/struct.pyi +0 -0
  129. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm_separate/EFR32MG/sys.pyi +0 -0
  130. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm_separate/EFR32MG/time.pyi +0 -0
  131. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm_separate/EFR32MG/ubinascii.pyi +0 -0
  132. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm_separate/EFR32MG/ucryptolib.pyi +0 -0
  133. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm_separate/EFR32MG/uerrno.pyi +0 -0
  134. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm_separate/EFR32MG/uhashlib.pyi +0 -0
  135. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm_separate/EFR32MG/uio.pyi +0 -0
  136. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm_separate/EFR32MG/ujson.pyi +0 -0
  137. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm_separate/EFR32MG/umachine.pyi +0 -0
  138. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm_separate/EFR32MG/uos.pyi +0 -0
  139. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm_separate/EFR32MG/uselect.pyi +0 -0
  140. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm_separate/EFR32MG/usocket.pyi +0 -0
  141. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm_separate/EFR32MG/ussl.pyi +0 -0
  142. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm_separate/EFR32MG/ustruct.pyi +0 -0
  143. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm_separate/EFR32MG/utime.pyi +0 -0
  144. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm_separate/EFR32MG/xbee/__init__.pyi +0 -0
  145. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm_separate/EFR32MG/xbee/modem_status.pyi +0 -0
  146. {replx-1.22.2 → replx-1.25.0}/replx/typehints/comm_separate/EFR32MG/xbee/relay.pyi +0 -0
  147. {replx-1.22.2 → replx-1.25.0}/replx/typehints/core/ESP32/aioespnow.pyi +0 -0
  148. {replx-1.22.2 → replx-1.25.0}/replx/typehints/core/ESP32/esp.pyi +0 -0
  149. {replx-1.22.2 → replx-1.25.0}/replx/typehints/core/ESP32/esp32.pyi +0 -0
  150. {replx-1.22.2 → replx-1.25.0}/replx/typehints/core/ESP32/espnow.pyi +0 -0
  151. {replx-1.22.2 → replx-1.25.0}/replx/typehints/core/MIMXRT1062DVJ6A/mimxrt.pyi +0 -0
  152. {replx-1.22.2 → replx-1.25.0}/replx/typehints/core/RP2350/rp2.pyi +0 -0
  153. {replx-1.22.2 → replx-1.25.0}/replx/utils/__init__.py +0 -0
  154. {replx-1.22.2 → replx-1.25.0}/replx/utils/constants.py +0 -0
  155. {replx-1.22.2 → replx-1.25.0}/replx/utils/device_info.py +0 -0
  156. {replx-1.22.2 → replx-1.25.0}/replx/utils/exceptions.py +0 -0
  157. {replx-1.22.2 → replx-1.25.0}/replx.egg-info/SOURCES.txt +0 -0
  158. {replx-1.22.2 → replx-1.25.0}/replx.egg-info/dependency_links.txt +0 -0
  159. {replx-1.22.2 → replx-1.25.0}/replx.egg-info/entry_points.txt +0 -0
  160. {replx-1.22.2 → replx-1.25.0}/replx.egg-info/requires.txt +0 -0
  161. {replx-1.22.2 → replx-1.25.0}/replx.egg-info/top_level.txt +0 -0
  162. {replx-1.22.2 → replx-1.25.0}/setup.cfg +0 -0
  163. {replx-1.22.2 → replx-1.25.0}/test/test_hardware_commands.py +0 -0
replx-1.25.0/PKG-INFO ADDED
@@ -0,0 +1,73 @@
1
+ Metadata-Version: 2.4
2
+ Name: replx
3
+ Version: 1.25.0
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@planxlab.com>
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://planxlab.com
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,gpio,i2c,spi,uart,pwm,adc,i2s,ws2812
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: click>=8.4.2
21
+ Requires-Dist: typer>=0.27.0
22
+ Requires-Dist: rich>=13.0
23
+ Requires-Dist: pyserial>=3.5
24
+ Requires-Dist: mpy-cross>=1.27.0
25
+ Requires-Dist: psutil>=7.2.2
26
+ Dynamic: license-file
27
+
28
+ # replx
29
+
30
+ [![PyPI version](https://badge.fury.io/py/replx.svg)](https://pypi.org/project/replx/)
31
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
32
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
33
+
34
+ **A cross-platform command-line toolkit for MicroPython development.** replx runs a shared local agent so multiple terminals can work with multiple boards while reusing connections and session state.
35
+
36
+ ## Features
37
+
38
+ - Works on Windows, Linux, and macOS.
39
+ - Connect to multiple boards from multiple terminals, with per-session foreground and background devices.
40
+ - Discover boards, configure a workspace default, and set up VS Code MicroPython support.
41
+ - Run scripts and snippets, use the interactive REPL, and sync files and directories.
42
+ - Search and install packages with `mip` or `pkg`; compile `.py` files to `.mpy`.
43
+ - Work with GPIO, PWM, ADC, UART, SPI, I2C, I2S, BLE, and WS2812 devices (availability depends on board and firmware).
44
+ - Use MicroPython raw-paste for code submissions of 4 KiB or larger when supported; batched file uploads keep the standard raw path for throughput.
45
+
46
+ ## Install
47
+
48
+ ```sh
49
+ python -m pip install replx
50
+ ```
51
+
52
+ This installs both `replx` and the `rx` command.
53
+
54
+ ## Quick start
55
+
56
+ ```sh
57
+ replx scan
58
+ replx COM4 setup
59
+ replx status
60
+ replx -c "print('Hello from MicroPython')"
61
+ replx run main.py
62
+ replx repl
63
+ ```
64
+
65
+ Replace `COM4` with the board's serial port, such as `/dev/ttyUSB0` on Linux or `/dev/cu.usbmodem14101` on macOS. After `setup`, the configured board can usually be targeted without repeating the port; specify a port when working with another board.
66
+
67
+ ## Documentation
68
+
69
+ This PyPI page is a short overview and quick start. See the [full command reference and user manual on GitHub](https://github.com/PlanXLab/replx) for connection/session behavior, commands, options, examples, and troubleshooting.
70
+
71
+ - [GitHub repository](https://github.com/PlanXLab/replx)
72
+ - [Report an issue](https://github.com/PlanXLab/replx/issues)
73
+ - [Website](https://planxlab.com)
replx-1.25.0/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # replx
2
+
3
+ [![PyPI version](https://badge.fury.io/py/replx.svg)](https://pypi.org/project/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
+ **A cross-platform command-line toolkit for MicroPython development.** replx runs a shared local agent so multiple terminals can work with multiple boards while reusing connections and session state.
8
+
9
+ ## Features
10
+
11
+ - Works on Windows, Linux, and macOS.
12
+ - Connect to multiple boards from multiple terminals, with per-session foreground and background devices.
13
+ - Discover boards, configure a workspace default, and set up VS Code MicroPython support.
14
+ - Run scripts and snippets, use the interactive REPL, and sync files and directories.
15
+ - Search and install packages with `mip` or `pkg`; compile `.py` files to `.mpy`.
16
+ - Work with GPIO, PWM, ADC, UART, SPI, I2C, I2S, BLE, and WS2812 devices (availability depends on board and firmware).
17
+ - Use MicroPython raw-paste for code submissions of 4 KiB or larger when supported; batched file uploads keep the standard raw path for throughput.
18
+
19
+ ## Install
20
+
21
+ ```sh
22
+ python -m pip install replx
23
+ ```
24
+
25
+ This installs both `replx` and the `rx` command.
26
+
27
+ ## Quick start
28
+
29
+ ```sh
30
+ replx scan
31
+ replx COM4 setup
32
+ replx status
33
+ replx -c "print('Hello from MicroPython')"
34
+ replx run main.py
35
+ replx repl
36
+ ```
37
+
38
+ Replace `COM4` with the board's serial port, such as `/dev/ttyUSB0` on Linux or `/dev/cu.usbmodem14101` on macOS. After `setup`, the configured board can usually be targeted without repeating the port; specify a port when working with another board.
39
+
40
+ ## Documentation
41
+
42
+ This PyPI page is a short overview and quick start. See the [full command reference and user manual on GitHub](https://github.com/PlanXLab/replx) for connection/session behavior, commands, options, examples, and troubleshooting.
43
+
44
+ - [GitHub repository](https://github.com/PlanXLab/replx)
45
+ - [Report an issue](https://github.com/PlanXLab/replx/issues)
46
+ - [Website](https://planxlab.com)
@@ -34,7 +34,7 @@ dependencies = [
34
34
  [project.optional-dependencies]
35
35
 
36
36
  [project.urls]
37
- Homepage = "https://github.com/PlanXLab/replx"
37
+ Homepage = "https://planxlab.com"
38
38
  Repository = "https://github.com/PlanXLab/replx"
39
39
  Issues = "https://github.com/PlanXLab/replx/issues"
40
40
 
@@ -1,5 +1,5 @@
1
1
  __all__ = ["__version__", "get_version", "__description__"]
2
- __version__ = "1.22.2"
2
+ __version__ = "1.25.0"
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
 
@@ -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
 
@@ -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,9 +2,10 @@ 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
@@ -58,7 +59,7 @@ class InteractiveSessionState:
58
59
  seq: int = 0
59
60
  client_addr: Optional[tuple] = None
60
61
  echo: bool = True
61
- input_queue: List[Any] = field(default_factory=list)
62
+ input_queue: Deque[bytes] = field(default_factory=deque)
62
63
  stop_requested: bool = False
63
64
  thread: Optional[threading.Thread] = None
64
65
  error: Optional[str] = None
@@ -71,7 +72,7 @@ class InteractiveSessionState:
71
72
  self.seq = seq
72
73
  self.client_addr = client_addr
73
74
  self.echo = echo
74
- self.input_queue = []
75
+ self.input_queue.clear()
75
76
  self.stop_requested = False
76
77
  self.error = None
77
78
 
@@ -82,7 +83,7 @@ class InteractiveSessionState:
82
83
  self.ppid = None
83
84
  self.seq = 0
84
85
  self.client_addr = None
85
- self.input_queue = []
86
+ self.input_queue.clear()
86
87
  self.stop_requested = False
87
88
  thread_to_join = self.thread
88
89
  self.thread = None
@@ -104,14 +105,14 @@ class ReplSessionState:
104
105
  ppid: Optional[int] = None
105
106
  reader_future: Optional[ConcurrentFuture] = None
106
107
  reader_thread: Optional[threading.Thread] = None
107
- output_buffer: bytes = b""
108
+ output_buffer: bytearray = field(default_factory=bytearray)
108
109
  buffer_lock: threading.Lock = field(default_factory=threading.Lock)
109
110
 
110
111
  def start(self, ppid: int):
111
112
  self.active = True
112
113
  self.ppid = ppid
113
114
  with self.buffer_lock:
114
- self.output_buffer = b""
115
+ self.output_buffer.clear()
115
116
 
116
117
  def stop(self):
117
118
  self.active = False
@@ -123,7 +124,7 @@ class ReplSessionState:
123
124
  self.reader_thread.join(timeout=1)
124
125
  self.reader_thread = None
125
126
  with self.buffer_lock:
126
- self.output_buffer = b""
127
+ self.output_buffer.clear()
127
128
 
128
129
  def is_owner(self, ppid: int) -> bool:
129
130
  if ppid is None:
@@ -134,13 +135,13 @@ class ReplSessionState:
134
135
 
135
136
  def append_output(self, data: bytes):
136
137
  with self.buffer_lock:
137
- self.output_buffer += data
138
+ self.output_buffer.extend(data)
138
139
 
139
140
  def read_output(self) -> bytes:
140
141
  with self.buffer_lock:
141
142
  data = self.output_buffer
142
- self.output_buffer = b""
143
- return data
143
+ self.output_buffer = bytearray()
144
+ return bytes(data)
144
145
 
145
146
  @dataclass
146
147
  class BoardConnection:
@@ -617,10 +617,14 @@ class AgentServer(
617
617
  self._cache_response(client_addr, seq, encoded)
618
618
  self._safe_send(encoded, client_addr)
619
619
 
620
- def _handle_request(self, data: bytes, client_addr: tuple):
620
+ def _handle_request(self, request: bytes | dict, client_addr: tuple):
621
621
  msg = None
622
622
  try:
623
- msg = AgentProtocol.decode_message(data)
623
+ msg = (
624
+ AgentProtocol.decode_message(request)
625
+ if isinstance(request, bytes)
626
+ else request
627
+ )
624
628
  if not msg:
625
629
  print(f"Invalid message from {client_addr}", file=sys.stderr)
626
630
  return
@@ -360,7 +360,7 @@ class ExecCommandsMixin:
360
360
  input_data = None
361
361
  with conn.interactive.lock:
362
362
  if conn.interactive.input_queue:
363
- input_data = conn.interactive.input_queue.pop(0)
363
+ input_data = conn.interactive.input_queue.popleft()
364
364
 
365
365
  if input_data:
366
366
  try:
@@ -17,6 +17,8 @@ FAST_COMMANDS: frozenset[str] = frozenset({
17
17
  'repl_read',
18
18
  })
19
19
 
20
+ _RESPONSE_CACHE_MAX_BYTES = 16 * 1024 * 1024
21
+
20
22
 
21
23
  class AgentDatagramProtocol(asyncio.DatagramProtocol):
22
24
  __slots__ = ('_server',)
@@ -44,7 +46,8 @@ class AgentDatagramProtocol(asyncio.DatagramProtocol):
44
46
  if command in FAST_COMMANDS
45
47
  else server._slow_executor
46
48
  )
47
- executor.submit(server._handle_request, data, addr)
49
+ request = msg if isinstance(msg, dict) else data
50
+ executor.submit(server._handle_request, request, addr)
48
51
 
49
52
  def error_received(self, exc: Exception) -> None:
50
53
  if self._server.running:
@@ -59,6 +62,7 @@ class UdpReliabilityMixin:
59
62
  self,
60
63
  *,
61
64
  response_cache_max: int = 512,
65
+ response_cache_max_bytes: int = _RESPONSE_CACHE_MAX_BYTES,
62
66
  last_seq_max: int = 256,
63
67
  ) -> None:
64
68
  self.last_seq: dict = {}
@@ -69,9 +73,10 @@ class UdpReliabilityMixin:
69
73
  self._send_lock = threading.Lock()
70
74
 
71
75
  self._response_cache: dict[tuple, bytes] = {}
72
- self._response_cache_order: list[tuple] = []
76
+ self._response_cache_bytes = 0
73
77
  self._response_cache_lock = threading.Lock()
74
78
  self._RESPONSE_CACHE_MAX = response_cache_max
79
+ self._RESPONSE_CACHE_MAX_BYTES = response_cache_max_bytes
75
80
 
76
81
  self._stream_ack_events: dict[tuple, threading.Event] = {}
77
82
  self._stream_ack_lock = threading.Lock()
@@ -92,17 +97,20 @@ class UdpReliabilityMixin:
92
97
  return
93
98
  key = (client_addr, seq)
94
99
  with self._response_cache_lock:
95
- if key in self._response_cache:
96
- try:
97
- self._response_cache_order.remove(key)
98
- except ValueError:
99
- pass
100
- else:
101
- if len(self._response_cache_order) >= self._RESPONSE_CACHE_MAX:
102
- oldest = self._response_cache_order.pop(0)
103
- self._response_cache.pop(oldest, None)
100
+ previous = self._response_cache.pop(key, None)
101
+ if previous is not None:
102
+ self._response_cache_bytes -= len(previous)
104
103
  self._response_cache[key] = data
105
- self._response_cache_order.append(key)
104
+ self._response_cache_bytes += len(data)
105
+
106
+ # Dict insertion order provides the FIFO index without a second list.
107
+ while (
108
+ len(self._response_cache) > self._RESPONSE_CACHE_MAX
109
+ or self._response_cache_bytes > self._RESPONSE_CACHE_MAX_BYTES
110
+ ):
111
+ oldest = next(iter(self._response_cache))
112
+ evicted = self._response_cache.pop(oldest)
113
+ self._response_cache_bytes -= len(evicted)
106
114
 
107
115
  def _lookup_response(self, client_addr: tuple, seq: int) -> Optional[bytes]:
108
116
  with self._response_cache_lock:
@@ -1536,7 +1536,7 @@ Start a live MicroPython session where you can type code line by line.
1536
1536
  break
1537
1537
  else:
1538
1538
  while True:
1539
- char = getch()
1539
+ char = _repl_getch()
1540
1540
 
1541
1541
  if char == b'\x00' or not char:
1542
1542
  continue
@@ -6,8 +6,6 @@ import threading
6
6
  from contextlib import contextmanager
7
7
  from typing import Callable, Optional
8
8
 
9
- import typer
10
-
11
9
  from replx.utils.exceptions import ProtocolError, TransportError
12
10
  from replx.utils.constants import (
13
11
  CTRL_A, CTRL_B, CTRL_C, CTRL_D, CTRL_E,
@@ -27,6 +25,8 @@ from replx.terminal import (
27
25
  getch, putch, getch_nonblock,
28
26
  )
29
27
 
28
+ _RAW_PASTE_MIN_COMMAND_SIZE = 4 * 1024
29
+
30
30
 
31
31
  class ReplProtocol:
32
32
  _active_instance = None
@@ -133,7 +133,7 @@ class ReplProtocol:
133
133
 
134
134
  def _read_ex(self, min_num_bytes:int, ending:bytes, timeout:int=5,
135
135
  data_consumer:Optional[Callable[[bytes], None]]=None) -> bytes:
136
- start = time.time()
136
+ start = time.monotonic()
137
137
  deadline = (start + timeout) if timeout > 0 else None
138
138
 
139
139
  streaming_mode = (timeout == 0 and data_consumer is not None)
@@ -265,18 +265,18 @@ class ReplProtocol:
265
265
  self._rx_pushback[:] = tail + self._rx_pushback
266
266
  return _get_result()
267
267
 
268
- last_activity = time.time()
269
- last_keepalive = time.time()
268
+ last_activity = time.monotonic()
269
+ last_keepalive = time.monotonic()
270
270
  keepalive_interval = 30.0
271
271
 
272
272
  while True:
273
273
  if self._stop_event.is_set():
274
274
  break
275
275
 
276
- if deadline is not None and time.time() >= deadline:
276
+ if deadline is not None and time.monotonic() >= deadline:
277
277
  break
278
278
 
279
- now = time.time()
279
+ now = time.monotonic()
280
280
  if streaming_mode and (now - last_keepalive) >= keepalive_interval:
281
281
  try:
282
282
  if hasattr(self.transport, 'keep_alive'):
@@ -294,7 +294,7 @@ class ReplProtocol:
294
294
  chunk = _read_some(want)
295
295
  if chunk:
296
296
  pos = _feed_chunk(chunk)
297
- last_activity = time.time()
297
+ last_activity = time.monotonic()
298
298
  if pos >= 0:
299
299
  tail = chunk[pos:]
300
300
  if tail:
@@ -307,7 +307,7 @@ class ReplProtocol:
307
307
 
308
308
  if timeout > 0:
309
309
  idle_limit = max(timeout * 2, 10)
310
- if (time.time() - last_activity) > idle_limit:
310
+ if (time.monotonic() - last_activity) > idle_limit:
311
311
  break
312
312
 
313
313
  return _get_result()
@@ -434,10 +434,10 @@ class ReplProtocol:
434
434
  try:
435
435
  self.transport.write(RAW_PASTE_INIT)
436
436
 
437
- start = time.time()
437
+ start = time.monotonic()
438
438
  timeout = 0.5
439
439
  response = b''
440
- while len(response) < 2 and (time.time() - start) < timeout:
440
+ while len(response) < 2 and (time.monotonic() - start) < timeout:
441
441
  waiting = self.transport.in_waiting()
442
442
  if waiting > 0:
443
443
  response += self._read(min(2 - len(response), waiting))
@@ -449,14 +449,16 @@ class ReplProtocol:
449
449
  return False
450
450
 
451
451
  if response == RAW_PASTE_SUPPORTED:
452
- init_bytes = self._read(3)
453
- if len(init_bytes) >= 2:
454
- self._raw_paste_window_size = struct.unpack('<H', init_bytes[:2])[0]
455
- self._raw_paste_supported = True
456
- return True
457
- else:
458
- self._raw_paste_supported = False
459
- return False
452
+ window_bytes = self._read(2)
453
+ if len(window_bytes) == 2:
454
+ window_size = struct.unpack('<H', window_bytes)[0]
455
+ if window_size > 0:
456
+ self._raw_paste_window_size = window_size
457
+ self._raw_paste_supported = True
458
+ return True
459
+
460
+ self._raw_paste_supported = False
461
+ return False
460
462
 
461
463
  elif response == RAW_PASTE_NOT_SUPPORTED:
462
464
  self._raw_paste_supported = False
@@ -513,13 +515,13 @@ class ReplProtocol:
513
515
  pass
514
516
 
515
517
  def _exec_raw_paste(self, command: bytes, data_consumer: Optional[Callable[[bytes], None]] = None) -> tuple[bytes, bytes]:
516
- remaining_window = self._raw_paste_window_size * 2
518
+ remaining_window = self._raw_paste_window_size
517
519
  bytes_sent = 0
518
520
  command_len = len(command)
519
521
  aborted = False
520
522
 
521
523
  while bytes_sent < command_len:
522
- while remaining_window <= 0:
524
+ while remaining_window <= 0 or self.transport.in_waiting() > 0:
523
525
  fc_byte = self._read(1)
524
526
  if fc_byte == RAW_PASTE_WINDOW_INC:
525
527
  remaining_window += self._raw_paste_window_size
@@ -527,27 +529,19 @@ class ReplProtocol:
527
529
  self.transport.write(CTRL_D)
528
530
  aborted = True
529
531
  break
530
-
532
+ elif fc_byte:
533
+ raise ProtocolError(
534
+ f"Unexpected raw-paste flow-control byte: {fc_byte!r}"
535
+ )
536
+
531
537
  if aborted:
532
538
  break
533
-
539
+
534
540
  chunk_size = min(remaining_window, command_len - bytes_sent)
535
541
  chunk = command[bytes_sent:bytes_sent + chunk_size]
536
542
  self.transport.write(chunk)
537
543
  bytes_sent += chunk_size
538
544
  remaining_window -= chunk_size
539
-
540
- while self.transport.in_waiting() > 0:
541
- fc_byte = self._read(1)
542
- if fc_byte == RAW_PASTE_WINDOW_INC:
543
- remaining_window += self._raw_paste_window_size
544
- elif fc_byte == RAW_PASTE_END_DATA:
545
- self.transport.write(CTRL_D)
546
- aborted = True
547
- break
548
-
549
- if aborted:
550
- break
551
545
 
552
546
  if not aborted and bytes_sent == command_len:
553
547
  self.transport.write(CTRL_D)
@@ -570,7 +564,7 @@ class ReplProtocol:
570
564
 
571
565
  def _exec(self, command:str|bytes, interactive:bool=False, echo:bool=False, detach:bool=False,
572
566
  data_consumer:Optional[Callable[[bytes], None]]=None,
573
- force_raw_paste:bool=False) -> bytes:
567
+ force_raw_paste:Optional[bool]=None) -> bytes:
574
568
  self._stop_event.clear()
575
569
  self._reset_error_filter()
576
570
 
@@ -591,7 +585,7 @@ class ReplProtocol:
591
585
 
592
586
  def _exec_command(self, command: bytes, interactive: bool, echo: bool, detach: bool,
593
587
  data_consumer: Optional[Callable[[bytes], None]],
594
- force_raw_paste: bool) -> bytes:
588
+ force_raw_paste: Optional[bool]) -> bytes:
595
589
  data_err = b''
596
590
  if data_consumer is None and interactive:
597
591
  data_consumer = self._create_data_consumer()
@@ -603,7 +597,10 @@ class ReplProtocol:
603
597
 
604
598
  command_len = len(command)
605
599
 
606
- use_raw_paste = force_raw_paste and self._enter_raw_paste_mode()
600
+ prefer_raw_paste = force_raw_paste
601
+ if prefer_raw_paste is None:
602
+ prefer_raw_paste = command_len >= _RAW_PASTE_MIN_COMMAND_SIZE
603
+ use_raw_paste = prefer_raw_paste and self._enter_raw_paste_mode()
607
604
 
608
605
  if use_raw_paste:
609
606
  try:
@@ -630,7 +627,7 @@ class ReplProtocol:
630
627
  current_buffer_size = self._DEVICE_CHUNK_SIZES
631
628
  bytes_sent = 0
632
629
 
633
- start_time = time.time()
630
+ start_time = time.monotonic()
634
631
 
635
632
  while bytes_sent < command_len:
636
633
  chunk_end = min(bytes_sent + current_buffer_size, command_len)
@@ -644,7 +641,7 @@ class ReplProtocol:
644
641
 
645
642
  self.transport.write(EOF_MARKER)
646
643
 
647
- transfer_time = time.time() - start_time
644
+ transfer_time = time.monotonic() - start_time
648
645
  timeout = max(5, int(transfer_time * 2))
649
646
 
650
647
  data = self._read_ex(1, OK_RESPONSE, timeout=timeout)
@@ -742,8 +739,8 @@ class ReplProtocol:
742
739
  return data_consumer
743
740
 
744
741
  def _drain_eof(self, max_ms:int=200):
745
- deadline = time.time() + max_ms / 1000
746
- while time.time() < deadline:
742
+ deadline = time.monotonic() + max_ms / 1000
743
+ while time.monotonic() < deadline:
747
744
  waiting = self.transport.in_waiting()
748
745
  if waiting:
749
746
  _ = self._read(waiting)
@@ -826,6 +823,8 @@ class ReplProtocol:
826
823
 
827
824
  def run(self, local, interactive:bool=False, echo:bool=False):
828
825
  if not interactive and echo:
826
+ import typer
827
+
829
828
  raise typer.BadParameter("Option chaining error: -n and -e can only be used once, not multiple times.")
830
829
 
831
830
  with open(local, "rb") as f:
@@ -900,8 +899,8 @@ class ReplProtocol:
900
899
  return
901
900
  time.sleep(0.1)
902
901
 
903
- drain_timeout = time.time() + 0.5
904
- while time.time() < drain_timeout:
902
+ drain_timeout = time.monotonic() + 0.5
903
+ while time.monotonic() < drain_timeout:
905
904
  if self.transport.in_waiting() > 0:
906
905
  self.transport.read(self.transport.in_waiting())
907
906
  time.sleep(0.02)
@@ -917,8 +916,8 @@ class ReplProtocol:
917
916
  print('')
918
917
  return
919
918
 
920
- prompt_timeout = time.time() + 1.0
921
- while time.time() < prompt_timeout:
919
+ prompt_timeout = time.monotonic() + 1.0
920
+ while time.monotonic() < prompt_timeout:
922
921
  time.sleep(0.05)
923
922
  if self._repl_prompt_detected:
924
923
  break
@@ -750,7 +750,8 @@ if chunk:
750
750
  return
751
751
 
752
752
  code = ";\n".join(batch_src_lines)
753
- self.repl._exec(code)
753
+ # Keep bulk file batches on raw REPL to avoid per-window flow-control overhead.
754
+ self.repl._exec(code, force_raw_paste=False)
754
755
  batch_src_lines = []
755
756
  batch_bytes = 0
756
757
 
@@ -199,7 +199,7 @@ else:
199
199
  _FD = sys.stdin.fileno()
200
200
 
201
201
  _terminal_state = threading.local()
202
- _terminal_lock = threading.Lock()
202
+ _terminal_lock = threading.RLock()
203
203
 
204
204
  def _get_terminal_state():
205
205
  if not hasattr(_terminal_state, 'old_settings'):
@@ -501,4 +501,3 @@ class LineModeTerminal:
501
501
  self._scroll_bottom = self._rows - 1
502
502
  lmt_set_scroll_region(self._scroll_top, self._scroll_bottom)
503
503
  self._draw_input()
504
-