replx 1.4__tar.gz → 1.6__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 (154) hide show
  1. {replx-1.4/replx.egg-info → replx-1.6}/PKG-INFO +1 -1
  2. {replx-1.4 → replx-1.6}/replx/__init__.py +1 -1
  3. {replx-1.4 → replx-1.6}/replx/cli/agent/client/core.py +7 -4
  4. {replx-1.4 → replx-1.6}/replx/cli/agent/server/connection_manager.py +9 -2
  5. {replx-1.4 → replx-1.6}/replx/cli/agent/server/handlers/exec.py +8 -5
  6. {replx-1.4 → replx-1.6}/replx/cli/agent/server/handlers/session.py +3 -6
  7. {replx-1.4 → replx-1.6}/replx/cli/commands/exec.py +3 -5
  8. {replx-1.4 → replx-1.6}/replx/cli/commands/utility.py +13 -13
  9. {replx-1.4 → replx-1.6/replx.egg-info}/PKG-INFO +1 -1
  10. replx-1.6/test/test_termio.py +70 -0
  11. replx-1.4/test/test_termio.py +0 -62
  12. {replx-1.4 → replx-1.6}/LICENSE +0 -0
  13. {replx-1.4 → replx-1.6}/README.md +0 -0
  14. {replx-1.4 → replx-1.6}/pyproject.toml +0 -0
  15. {replx-1.4 → replx-1.6}/replx/cli/__init__.py +0 -0
  16. {replx-1.4 → replx-1.6}/replx/cli/agent/__init__.py +0 -0
  17. {replx-1.4 → replx-1.6}/replx/cli/agent/client/__init__.py +0 -0
  18. {replx-1.4 → replx-1.6}/replx/cli/agent/client/session.py +0 -0
  19. {replx-1.4 → replx-1.6}/replx/cli/agent/protocol.py +0 -0
  20. {replx-1.4 → replx-1.6}/replx/cli/agent/server/__init__.py +0 -0
  21. {replx-1.4 → replx-1.6}/replx/cli/agent/server/__main__.py +0 -0
  22. {replx-1.4 → replx-1.6}/replx/cli/agent/server/command_dispatcher.py +0 -0
  23. {replx-1.4 → replx-1.6}/replx/cli/agent/server/core.py +0 -0
  24. {replx-1.4 → replx-1.6}/replx/cli/agent/server/handlers/__init__.py +0 -0
  25. {replx-1.4 → replx-1.6}/replx/cli/agent/server/handlers/filesystem.py +0 -0
  26. {replx-1.4 → replx-1.6}/replx/cli/agent/server/handlers/repl.py +0 -0
  27. {replx-1.4 → replx-1.6}/replx/cli/agent/server/handlers/transfer.py +0 -0
  28. {replx-1.4 → replx-1.6}/replx/cli/agent/server/session_manager.py +0 -0
  29. {replx-1.4 → replx-1.6}/replx/cli/app.py +0 -0
  30. {replx-1.4 → replx-1.6}/replx/cli/commands/__init__.py +0 -0
  31. {replx-1.4 → replx-1.6}/replx/cli/commands/device.py +0 -0
  32. {replx-1.4 → replx-1.6}/replx/cli/commands/file.py +0 -0
  33. {replx-1.4 → replx-1.6}/replx/cli/commands/firmware.py +0 -0
  34. {replx-1.4 → replx-1.6}/replx/cli/commands/package.py +0 -0
  35. {replx-1.4 → replx-1.6}/replx/cli/config.py +0 -0
  36. {replx-1.4 → replx-1.6}/replx/cli/connection.py +0 -0
  37. {replx-1.4 → replx-1.6}/replx/cli/helpers/__init__.py +0 -0
  38. {replx-1.4 → replx-1.6}/replx/cli/helpers/compiler.py +0 -0
  39. {replx-1.4 → replx-1.6}/replx/cli/helpers/environment.py +0 -0
  40. {replx-1.4 → replx-1.6}/replx/cli/helpers/output.py +0 -0
  41. {replx-1.4 → replx-1.6}/replx/cli/helpers/registry.py +0 -0
  42. {replx-1.4 → replx-1.6}/replx/cli/helpers/scanner.py +0 -0
  43. {replx-1.4 → replx-1.6}/replx/cli/helpers/store.py +0 -0
  44. {replx-1.4 → replx-1.6}/replx/cli/helpers/updater.py +0 -0
  45. {replx-1.4 → replx-1.6}/replx/commands.py +0 -0
  46. {replx-1.4 → replx-1.6}/replx/protocol/__init__.py +0 -0
  47. {replx-1.4 → replx-1.6}/replx/protocol/repl.py +0 -0
  48. {replx-1.4 → replx-1.6}/replx/protocol/storage.py +0 -0
  49. {replx-1.4 → replx-1.6}/replx/terminal.py +0 -0
  50. {replx-1.4 → replx-1.6}/replx/tests/__init__.py +0 -0
  51. {replx-1.4 → replx-1.6}/replx/tests/test_agent_asyncio.py +0 -0
  52. {replx-1.4 → replx-1.6}/replx/tests/test_agent_port_canonicalization.py +0 -0
  53. {replx-1.4 → replx-1.6}/replx/tests/test_agent_thread_pool.py +0 -0
  54. {replx-1.4 → replx-1.6}/replx/tests/test_compiler_arch.py +0 -0
  55. {replx-1.4 → replx-1.6}/replx/tests/test_connection_info_lookup.py +0 -0
  56. {replx-1.4 → replx-1.6}/replx/tests/test_device_info_esp_multi_core.py +0 -0
  57. {replx-1.4 → replx-1.6}/replx/tests/test_disconnect_cleanup.py +0 -0
  58. {replx-1.4 → replx-1.6}/replx/tests/test_lock_cleanup.py +0 -0
  59. {replx-1.4 → replx-1.6}/replx/tests/test_pkg_local_version.py +0 -0
  60. {replx-1.4 → replx-1.6}/replx/tests/test_pkg_search_scope_filter.py +0 -0
  61. {replx-1.4 → replx-1.6}/replx/tests/test_repl_reader_task.py +0 -0
  62. {replx-1.4 → replx-1.6}/replx/tests/test_session_disconnect_releases_shared_port.py +0 -0
  63. {replx-1.4 → replx-1.6}/replx/tests/test_session_id_fallback.py +0 -0
  64. {replx-1.4 → replx-1.6}/replx/tests/test_shutdown_status_message.py +0 -0
  65. {replx-1.4 → replx-1.6}/replx/tests/test_windows_com_port_normalization.py +0 -0
  66. {replx-1.4 → replx-1.6}/replx/transport/__init__.py +0 -0
  67. {replx-1.4 → replx-1.6}/replx/transport/base.py +0 -0
  68. {replx-1.4 → replx-1.6}/replx/transport/serial.py +0 -0
  69. {replx-1.4 → replx-1.6}/replx/typehints/comm/_thread.pyi +0 -0
  70. {replx-1.4 → replx-1.6}/replx/typehints/comm/aioble/__init__.pyi +0 -0
  71. {replx-1.4 → replx-1.6}/replx/typehints/comm/array.pyi +0 -0
  72. {replx-1.4 → replx-1.6}/replx/typehints/comm/asyncio/__init__.pyi +0 -0
  73. {replx-1.4 → replx-1.6}/replx/typehints/comm/binascii.pyi +0 -0
  74. {replx-1.4 → replx-1.6}/replx/typehints/comm/bluetooth.pyi +0 -0
  75. {replx-1.4 → replx-1.6}/replx/typehints/comm/builtins.pyi +0 -0
  76. {replx-1.4 → replx-1.6}/replx/typehints/comm/cmath.pyi +0 -0
  77. {replx-1.4 → replx-1.6}/replx/typehints/comm/collections.pyi +0 -0
  78. {replx-1.4 → replx-1.6}/replx/typehints/comm/cryptolib.pyi +0 -0
  79. {replx-1.4 → replx-1.6}/replx/typehints/comm/deflate.pyi +0 -0
  80. {replx-1.4 → replx-1.6}/replx/typehints/comm/errno.pyi +0 -0
  81. {replx-1.4 → replx-1.6}/replx/typehints/comm/framebuf.pyi +0 -0
  82. {replx-1.4 → replx-1.6}/replx/typehints/comm/gc.pyi +0 -0
  83. {replx-1.4 → replx-1.6}/replx/typehints/comm/hashlib.pyi +0 -0
  84. {replx-1.4 → replx-1.6}/replx/typehints/comm/heapq.pyi +0 -0
  85. {replx-1.4 → replx-1.6}/replx/typehints/comm/io.pyi +0 -0
  86. {replx-1.4 → replx-1.6}/replx/typehints/comm/json.pyi +0 -0
  87. {replx-1.4 → replx-1.6}/replx/typehints/comm/lwip.pyi +0 -0
  88. {replx-1.4 → replx-1.6}/replx/typehints/comm/machine.pyi +0 -0
  89. {replx-1.4 → replx-1.6}/replx/typehints/comm/math.pyi +0 -0
  90. {replx-1.4 → replx-1.6}/replx/typehints/comm/micropython.pyi +0 -0
  91. {replx-1.4 → replx-1.6}/replx/typehints/comm/mip/__init__.pyi +0 -0
  92. {replx-1.4 → replx-1.6}/replx/typehints/comm/network.pyi +0 -0
  93. {replx-1.4 → replx-1.6}/replx/typehints/comm/ntptime.pyi +0 -0
  94. {replx-1.4 → replx-1.6}/replx/typehints/comm/os.pyi +0 -0
  95. {replx-1.4 → replx-1.6}/replx/typehints/comm/platform.pyi +0 -0
  96. {replx-1.4 → replx-1.6}/replx/typehints/comm/random.pyi +0 -0
  97. {replx-1.4 → replx-1.6}/replx/typehints/comm/re.pyi +0 -0
  98. {replx-1.4 → replx-1.6}/replx/typehints/comm/requests/__init__.pyi +0 -0
  99. {replx-1.4 → replx-1.6}/replx/typehints/comm/select.pyi +0 -0
  100. {replx-1.4 → replx-1.6}/replx/typehints/comm/socket.pyi +0 -0
  101. {replx-1.4 → replx-1.6}/replx/typehints/comm/ssl.pyi +0 -0
  102. {replx-1.4 → replx-1.6}/replx/typehints/comm/struct.pyi +0 -0
  103. {replx-1.4 → replx-1.6}/replx/typehints/comm/sys.pyi +0 -0
  104. {replx-1.4 → replx-1.6}/replx/typehints/comm/time.pyi +0 -0
  105. {replx-1.4 → replx-1.6}/replx/typehints/comm/tls.pyi +0 -0
  106. {replx-1.4 → replx-1.6}/replx/typehints/comm/uasyncio.pyi +0 -0
  107. {replx-1.4 → replx-1.6}/replx/typehints/comm/uctypes.pyi +0 -0
  108. {replx-1.4 → replx-1.6}/replx/typehints/comm/urequests.pyi +0 -0
  109. {replx-1.4 → replx-1.6}/replx/typehints/comm/vfs.pyi +0 -0
  110. {replx-1.4 → replx-1.6}/replx/typehints/comm_separate/EFR32MG/binascii.pyi +0 -0
  111. {replx-1.4 → replx-1.6}/replx/typehints/comm_separate/EFR32MG/errno.pyi +0 -0
  112. {replx-1.4 → replx-1.6}/replx/typehints/comm_separate/EFR32MG/hashlib.pyi +0 -0
  113. {replx-1.4 → replx-1.6}/replx/typehints/comm_separate/EFR32MG/io.pyi +0 -0
  114. {replx-1.4 → replx-1.6}/replx/typehints/comm_separate/EFR32MG/json.pyi +0 -0
  115. {replx-1.4 → replx-1.6}/replx/typehints/comm_separate/EFR32MG/machine.pyi +0 -0
  116. {replx-1.4 → replx-1.6}/replx/typehints/comm_separate/EFR32MG/math.pyi +0 -0
  117. {replx-1.4 → replx-1.6}/replx/typehints/comm_separate/EFR32MG/micropython.pyi +0 -0
  118. {replx-1.4 → replx-1.6}/replx/typehints/comm_separate/EFR32MG/network.pyi +0 -0
  119. {replx-1.4 → replx-1.6}/replx/typehints/comm_separate/EFR32MG/os.pyi +0 -0
  120. {replx-1.4 → replx-1.6}/replx/typehints/comm_separate/EFR32MG/select.pyi +0 -0
  121. {replx-1.4 → replx-1.6}/replx/typehints/comm_separate/EFR32MG/socket.pyi +0 -0
  122. {replx-1.4 → replx-1.6}/replx/typehints/comm_separate/EFR32MG/ssl.pyi +0 -0
  123. {replx-1.4 → replx-1.6}/replx/typehints/comm_separate/EFR32MG/struct.pyi +0 -0
  124. {replx-1.4 → replx-1.6}/replx/typehints/comm_separate/EFR32MG/sys.pyi +0 -0
  125. {replx-1.4 → replx-1.6}/replx/typehints/comm_separate/EFR32MG/time.pyi +0 -0
  126. {replx-1.4 → replx-1.6}/replx/typehints/comm_separate/EFR32MG/ubinascii.pyi +0 -0
  127. {replx-1.4 → replx-1.6}/replx/typehints/comm_separate/EFR32MG/ucryptolib.pyi +0 -0
  128. {replx-1.4 → replx-1.6}/replx/typehints/comm_separate/EFR32MG/uerrno.pyi +0 -0
  129. {replx-1.4 → replx-1.6}/replx/typehints/comm_separate/EFR32MG/uhashlib.pyi +0 -0
  130. {replx-1.4 → replx-1.6}/replx/typehints/comm_separate/EFR32MG/uio.pyi +0 -0
  131. {replx-1.4 → replx-1.6}/replx/typehints/comm_separate/EFR32MG/ujson.pyi +0 -0
  132. {replx-1.4 → replx-1.6}/replx/typehints/comm_separate/EFR32MG/umachine.pyi +0 -0
  133. {replx-1.4 → replx-1.6}/replx/typehints/comm_separate/EFR32MG/uos.pyi +0 -0
  134. {replx-1.4 → replx-1.6}/replx/typehints/comm_separate/EFR32MG/uselect.pyi +0 -0
  135. {replx-1.4 → replx-1.6}/replx/typehints/comm_separate/EFR32MG/usocket.pyi +0 -0
  136. {replx-1.4 → replx-1.6}/replx/typehints/comm_separate/EFR32MG/ussl.pyi +0 -0
  137. {replx-1.4 → replx-1.6}/replx/typehints/comm_separate/EFR32MG/ustruct.pyi +0 -0
  138. {replx-1.4 → replx-1.6}/replx/typehints/comm_separate/EFR32MG/utime.pyi +0 -0
  139. {replx-1.4 → replx-1.6}/replx/typehints/core/ESP32/aioespnow.pyi +0 -0
  140. {replx-1.4 → replx-1.6}/replx/typehints/core/ESP32/esp.pyi +0 -0
  141. {replx-1.4 → replx-1.6}/replx/typehints/core/ESP32/esp32.pyi +0 -0
  142. {replx-1.4 → replx-1.6}/replx/typehints/core/ESP32/espnow.pyi +0 -0
  143. {replx-1.4 → replx-1.6}/replx/typehints/core/MIMXRT1062DVJ6A/mimxrt.pyi +0 -0
  144. {replx-1.4 → replx-1.6}/replx/typehints/core/RP2350/rp2.pyi +0 -0
  145. {replx-1.4 → replx-1.6}/replx/utils/__init__.py +0 -0
  146. {replx-1.4 → replx-1.6}/replx/utils/constants.py +0 -0
  147. {replx-1.4 → replx-1.6}/replx/utils/device_info.py +0 -0
  148. {replx-1.4 → replx-1.6}/replx/utils/exceptions.py +0 -0
  149. {replx-1.4 → replx-1.6}/replx.egg-info/SOURCES.txt +0 -0
  150. {replx-1.4 → replx-1.6}/replx.egg-info/dependency_links.txt +0 -0
  151. {replx-1.4 → replx-1.6}/replx.egg-info/entry_points.txt +0 -0
  152. {replx-1.4 → replx-1.6}/replx.egg-info/requires.txt +0 -0
  153. {replx-1.4 → replx-1.6}/replx.egg-info/top_level.txt +0 -0
  154. {replx-1.4 → replx-1.6}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: replx
3
- Version: 1.4
3
+ Version: 1.6
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@gmail.com>
6
6
  License-Expression: MIT
@@ -1,5 +1,5 @@
1
1
  __all__ = ["__version__", "get_version", "__description__"]
2
- __version__ = "1.4"
2
+ __version__ = "1.6"
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
 
@@ -326,13 +326,14 @@ class AgentClient:
326
326
  if port:
327
327
  cmd.append(str(port))
328
328
 
329
+ proc = None
329
330
  if background:
330
331
  if sys.platform == 'win32':
331
332
  startupinfo = subprocess.STARTUPINFO()
332
333
  startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
333
334
  startupinfo.wShowWindow = 0
334
335
 
335
- subprocess.Popen(
336
+ proc = subprocess.Popen(
336
337
  cmd,
337
338
  creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.DETACHED_PROCESS,
338
339
  stdout=subprocess.DEVNULL,
@@ -340,7 +341,7 @@ class AgentClient:
340
341
  startupinfo=startupinfo
341
342
  )
342
343
  else:
343
- subprocess.Popen(
344
+ proc = subprocess.Popen(
344
345
  cmd,
345
346
  stdout=subprocess.DEVNULL,
346
347
  stderr=subprocess.DEVNULL,
@@ -349,10 +350,12 @@ class AgentClient:
349
350
  close_fds=True
350
351
  )
351
352
  else:
352
- subprocess.Popen(cmd)
353
+ proc = subprocess.Popen(cmd)
353
354
 
354
- for i in range(30):
355
+ for _ in range(100):
355
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})")
356
359
  if AgentClient.is_agent_running(port=port):
357
360
  return True
358
361
 
@@ -455,6 +455,10 @@ class ConnectionManager:
455
455
 
456
456
  conn = self._connections.pop(key)
457
457
 
458
+ # Remember whether a detached (user) script was running before clearing the flag.
459
+ # If so, the device is executing paste-mode code on its UART, NOT in raw REPL.
460
+ # Sending CTRL_B in that state would inject \x02 into the running script's stdin.
461
+ was_detached = conn.is_detached()
458
462
  conn.stop_detached()
459
463
 
460
464
  if conn.repl.active:
@@ -469,8 +473,11 @@ class ConnectionManager:
469
473
  transport = conn.repl_protocol.transport
470
474
  if transport:
471
475
  try:
472
- transport.write(CTRL_B)
473
- time.sleep(0.1 if sys.platform == 'win32' else 0.05)
476
+ if not was_detached:
477
+ # Only send CTRL_B to leave raw REPL when no user script
478
+ # was running; otherwise the byte contaminates the script stdin.
479
+ transport.write(CTRL_B)
480
+ time.sleep(0.1 if sys.platform == 'win32' else 0.05)
474
481
  except Exception:
475
482
  pass
476
483
  transport.close()
@@ -143,11 +143,14 @@ class ExecCommandsMixin:
143
143
  self.connection_manager.disconnect_all()
144
144
  self.session_manager.clear_all_sessions()
145
145
 
146
- if self.server_socket:
147
- try:
148
- self.server_socket.close()
149
- except Exception:
150
- pass
146
+ # Defer actual cleanup so the response can be sent before the transport closes.
147
+ # cleanup() properly sets _stop_event, which stops the asyncio loop and
148
+ # releases the UDP port — preventing "Address already in use" on next start.
149
+ def _deferred_cleanup():
150
+ time.sleep(0.2)
151
+ self.cleanup()
152
+
153
+ threading.Thread(target=_deferred_cleanup, daemon=True).start()
151
154
 
152
155
  return {"shutdown": True}
153
156
 
@@ -141,12 +141,9 @@ class SessionCommandsMixin:
141
141
  if not self.session_manager.has_sessions():
142
142
  def delayed_shutdown():
143
143
  time.sleep(0.3)
144
- self.running = False
145
- if self.server_socket:
146
- try:
147
- self.server_socket.close()
148
- except Exception:
149
- pass
144
+ # cleanup() properly sets _stop_event to stop the asyncio loop
145
+ # and releases the UDP port so a new agent can bind it immediately.
146
+ self.cleanup()
150
147
 
151
148
  shutdown_thread = threading.Thread(target=delayed_shutdown, daemon=True)
152
149
  shutdown_thread.start()
@@ -344,13 +344,11 @@ By default, runs a file from your computer. Use -d to run from device.
344
344
  if stream_type == "stderr":
345
345
  stderr_buffer.extend(data)
346
346
  else:
347
- # Normalize line endings for all platforms
348
347
  if not IS_WINDOWS:
349
- # Unix: CR+LF -> LF, then remove standalone CR
350
- data = data.replace(b'\r\n', b'\n')
351
- data = data.replace(b'\r', b'')
348
+ data = data.replace(b'\r\n', b'\n') # CRLF -> LF
349
+ data = data.replace(b'\r', b'\n') # lone CR -> LF
350
+ data = data.replace(b'\n', b'\r\n') # LF -> CRLF
352
351
  else:
353
- # Windows: just remove CR (terminal handles LF)
354
352
  data = data.replace(b'\r', b'')
355
353
 
356
354
  if data:
@@ -978,7 +978,8 @@ Format (erase) the filesystem on the connected device.
978
978
 
979
979
  [bold cyan]After formatting:[/bold cyan]
980
980
  • Device has empty filesystem
981
- • Reinstall libraries: [green]replx pkg update[/green]
981
+ • Reinstall libraries: [green]replx pkg update core.all[/green]
982
+ • Install device libs: [green]replx pkg update device.all[/green]
982
983
  • Or use [green]replx init[/green] instead (format + install)
983
984
 
984
985
  [bold cyan]Related:[/bold cyan]
@@ -1088,8 +1089,6 @@ def init(
1088
1089
  help_text = """\
1089
1090
  Completely reset device: format filesystem and install all libraries.
1090
1091
 
1091
- [bold yellow]⚠ WARNING: This deletes ALL files and reinstalls from scratch![/bold yellow]
1092
-
1093
1092
  [bold cyan]Usage:[/bold cyan]
1094
1093
  replx init
1095
1094
 
@@ -1114,13 +1113,15 @@ Completely reset device: format filesystem and install all libraries.
1114
1113
  • Or set up workspace: [green]replx setup[/green]
1115
1114
 
1116
1115
  [bold cyan]Equivalent to:[/bold cyan]
1117
- replx pkg download [dim]# Download libraries (if needed)[/dim]
1118
- replx format [dim]# Erase device storage[/dim]
1119
- replx pkg update [dim]# Install to device[/dim]
1116
+ replx pkg download [dim]# Download libraries (if needed)[/dim]
1117
+ replx format [dim]# Erase device storage[/dim]
1118
+ replx pkg update core.all [dim]# Install all core libs[/dim]
1119
+ replx pkg update device.all [dim]# Install all device libs[/dim]
1120
1120
 
1121
1121
  [bold cyan]Related:[/bold cyan]
1122
- replx format [dim]# Just erase (no install)[/dim]
1123
- replx pkg update [dim]# Just install (no format)[/dim]"""
1122
+ replx format [dim]# Just erase (no install)[/dim]
1123
+ replx pkg update core.all [dim]# Just install core (no format)[/dim]
1124
+ replx pkg update device.all [dim]# Just install device (no format)[/dim]"""
1124
1125
  OutputHelper.print_panel(help_text, border_style="dim")
1125
1126
  console.print()
1126
1127
  raise typer.Exit()
@@ -1270,11 +1271,11 @@ Completely reset device: format filesystem and install all libraries.
1270
1271
  install_stats = {}
1271
1272
 
1272
1273
  try:
1273
- specs_to_install = ["core/"]
1274
+ specs_to_install = ["core.all"]
1274
1275
 
1275
1276
  dev_src = os.path.join(StoreManager.pkg_root(), "device", device_name_to_path(STATE.device), "src")
1276
1277
  if os.path.isdir(dev_src):
1277
- specs_to_install.append("device/")
1278
+ specs_to_install.append("device.all")
1278
1279
 
1279
1280
  from rich.panel import Panel as RichPanel
1280
1281
  initial_panel = RichPanel(
@@ -1290,11 +1291,10 @@ Completely reset device: format filesystem and install all libraries.
1290
1291
  install_stats[spec_item] = result
1291
1292
 
1292
1293
  summary_parts = []
1293
- for spec_key in ["core/", "device/"]:
1294
+ for spec_key in ["core.all", "device.all"]:
1294
1295
  if spec_key in install_stats:
1295
1296
  stats = install_stats[spec_key]
1296
- label = spec_key.rstrip("/")
1297
- summary_parts.append(f"🔧 {label}/ {stats['files']} file(s) {format_bytes(stats['bytes'])}")
1297
+ summary_parts.append(f"🔧 {spec_key} {stats['files']} file(s) {format_bytes(stats['bytes'])}")
1298
1298
 
1299
1299
  summary_line = " ".join(summary_parts)
1300
1300
  live.update(RichPanel(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: replx
3
- Version: 1.4
3
+ Version: 1.6
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@gmail.com>
6
6
  License-Expression: MIT
@@ -0,0 +1,70 @@
1
+ from termio import ReplSerial
2
+ from utools import rand, clamp, intervalChecker
3
+ import time
4
+
5
+
6
+ def new_token() -> str:
7
+ return f"{rand(4):08x}" # 예: "a3f2c1b4"
8
+
9
+
10
+ with ReplSerial(timeout=0) as ser:
11
+ token = new_token()
12
+ authed = False
13
+ every_sess = intervalChecker(1000) # 미인증: 1초마다 SESSION 재전송
14
+ heartbeat = intervalChecker(5000) # 인증 후: 5초마다 ALIVE
15
+
16
+ # 최초 토큰 전송
17
+ ser.write(f"SESSION={token}\r\n".encode())
18
+
19
+ while True:
20
+ # timeout=0이므로 \r\n이 두 pump 사이클에 걸려 도착할 수 있음 → b"\n" 사용
21
+ line = ser.read_until(b"\n", max_size=64)
22
+ if line and line.endswith(b"\n"):
23
+ cmd = line.decode("utf-8", "replace").strip()
24
+
25
+ # 인증 (항상 허용)
26
+ if cmd.startswith("AUTH "):
27
+ if cmd[5:] == token:
28
+ authed = True
29
+ ser.write(b"OK authenticated\r\n")
30
+ else:
31
+ authed = False
32
+ ser.write(b"ERR wrong token\r\n")
33
+
34
+ # 미인증 요청 거부
35
+ elif not authed:
36
+ ser.write(b"ERR not authenticated\r\n")
37
+
38
+ # 인증 후 커맨드
39
+ elif cmd.startswith("SET_PWM:"):
40
+ try:
41
+ duty = clamp(int(cmd[8:]), 0, 100)
42
+ # pwm.duty(int(duty * 655.35)) # 0–100% → 0–65535
43
+ ser.write(f"OK pwm={duty}%\r\n".encode())
44
+ except (ValueError, TypeError):
45
+ ser.write(b"ERR bad value\r\n")
46
+
47
+ elif cmd.startswith("SET_FREQ:"):
48
+ try:
49
+ freq = clamp(int(cmd[9:]), 1, 20_000)
50
+ # pwm.freq(freq)
51
+ ser.write(f"OK freq={freq}Hz\r\n".encode())
52
+ except (ValueError, TypeError):
53
+ ser.write(b"ERR bad value\r\n")
54
+
55
+ elif cmd == "RESET_SESSION":
56
+ token = new_token()
57
+ authed = False
58
+ every_sess = intervalChecker(1000)
59
+ ser.write(f"SESSION={token}\r\n".encode())
60
+
61
+ else:
62
+ ser.write(b"ERR unknown cmd\r\n")
63
+
64
+ # 주기적 전송 (커맨드 수신 여부와 무관)
65
+ if not authed and every_sess():
66
+ ser.write(f"SESSION={token}\r\n".encode())
67
+ elif authed and heartbeat():
68
+ ser.write(b"ALIVE\r\n")
69
+
70
+ time.sleep_ms(10)
@@ -1,62 +0,0 @@
1
- from termio import ReplSerial
2
- import time
3
-
4
-
5
- def _drain_into(ser, rx_buf, per_loop_budget=128, read_chunk=32, max_rx_buf=512):
6
- drained = 0
7
-
8
- while True:
9
- n = ser.in_waiting
10
- if n <= 0:
11
- break
12
- if drained >= per_loop_budget:
13
- break
14
-
15
- # 한 번에 너무 크게 읽지 말고(파싱 지연/메모리), 일정 chunk로 끊어서 읽기
16
- want = n
17
- if want > read_chunk:
18
- want = read_chunk
19
- if want > (per_loop_budget - drained):
20
- want = per_loop_budget - drained
21
-
22
- chunk = ser.read(want) # timeout=0 이므로 즉시 반환
23
- if not chunk:
24
- break
25
- rx_buf.extend(chunk)
26
- drained += len(chunk)
27
-
28
- # 상한 초과 시 오래된 데이터부터 버려 폭주 방지(실전에서는 정책을 명확히 정하세요)
29
- if len(rx_buf) > max_rx_buf:
30
- rx_buf = rx_buf[-max_rx_buf:]
31
-
32
- return rx_buf, drained
33
-
34
-
35
- with ReplSerial(timeout=0) as ser:
36
- rx = bytearray()
37
- last_tick = time.ticks_ms()
38
-
39
- while True:
40
- # (예시) 메인 루프 주기 작업: 200ms마다 tick
41
- if time.ticks_diff(time.ticks_ms(), last_tick) >= 200:
42
- last_tick = time.ticks_ms()
43
- # print("tick") # 필요 시만 사용(로그가 수신을 가립니다)
44
-
45
- # 1) 수신 버퍼에 쌓인 데이터를 "가능한 만큼" 꺼내 임시 버퍼(rx)에 담기
46
- # (단, 이번 루프에서는 최대 per_loop_budget 바이트까지만 처리)
47
- rx, drained = _drain_into(ser, rx, 128, 32, 512)
48
-
49
- # 2) 완성된 프레임(\r)만 꺼내 처리
50
- while True:
51
- i = rx.find(b"\r")
52
- if i < 0:
53
- break
54
-
55
- frame = bytes(rx[:i])
56
- rx = rx[i + 1:]
57
-
58
- text = frame.decode("utf-8", "replace")
59
- print("[FRAME]", text)
60
-
61
- # 3) 다른 일을 할 시간을 양보
62
- time.sleep_ms(10)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes