com2tty 0.1.1__tar.gz → 0.1.2__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 (27) hide show
  1. {com2tty-0.1.1/src/com2tty.egg-info → com2tty-0.1.2}/PKG-INFO +1 -1
  2. {com2tty-0.1.1 → com2tty-0.1.2}/pyproject.toml +1 -1
  3. com2tty-0.1.2/src/com2tty/__init__.py +1 -0
  4. {com2tty-0.1.1 → com2tty-0.1.2}/src/com2tty/bridge.py +198 -5
  5. com2tty-0.1.2/src/com2tty/host.py +879 -0
  6. {com2tty-0.1.1 → com2tty-0.1.2/src/com2tty.egg-info}/PKG-INFO +1 -1
  7. com2tty-0.1.2/tests/test_bridge_script.py +1070 -0
  8. com2tty-0.1.2/tests/test_host.py +1803 -0
  9. com2tty-0.1.1/src/com2tty/__init__.py +0 -1
  10. com2tty-0.1.1/src/com2tty/host.py +0 -424
  11. com2tty-0.1.1/tests/test_bridge_script.py +0 -564
  12. com2tty-0.1.1/tests/test_host.py +0 -544
  13. {com2tty-0.1.1 → com2tty-0.1.2}/LICENSE +0 -0
  14. {com2tty-0.1.1 → com2tty-0.1.2}/README.md +0 -0
  15. {com2tty-0.1.1 → com2tty-0.1.2}/setup.cfg +0 -0
  16. {com2tty-0.1.1 → com2tty-0.1.2}/setup.py +0 -0
  17. {com2tty-0.1.1 → com2tty-0.1.2}/src/com2tty/__main__.py +0 -0
  18. {com2tty-0.1.1 → com2tty-0.1.2}/src/com2tty/cli.py +0 -0
  19. {com2tty-0.1.1 → com2tty-0.1.2}/src/com2tty/rfc2217_server.py +0 -0
  20. {com2tty-0.1.1 → com2tty-0.1.2}/src/com2tty.egg-info/SOURCES.txt +0 -0
  21. {com2tty-0.1.1 → com2tty-0.1.2}/src/com2tty.egg-info/dependency_links.txt +0 -0
  22. {com2tty-0.1.1 → com2tty-0.1.2}/src/com2tty.egg-info/entry_points.txt +0 -0
  23. {com2tty-0.1.1 → com2tty-0.1.2}/src/com2tty.egg-info/requires.txt +0 -0
  24. {com2tty-0.1.1 → com2tty-0.1.2}/src/com2tty.egg-info/top_level.txt +0 -0
  25. {com2tty-0.1.1 → com2tty-0.1.2}/tests/test_cli.py +0 -0
  26. {com2tty-0.1.1 → com2tty-0.1.2}/tests/test_main.py +0 -0
  27. {com2tty-0.1.1 → com2tty-0.1.2}/tests/test_rfc2217_server.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: com2tty
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: A Windows COM port to WSL ttyUSB forwarder
5
5
  Author-email: yichengs <yichengs.tw+com2tty@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/Yi-Cheng-Wang/com2tty
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "com2tty"
7
- version = "0.1.1"
7
+ version = "0.1.2"
8
8
  description = "A Windows COM port to WSL ttyUSB forwarder"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -0,0 +1 @@
1
+ __version__ = "0.1.2"
@@ -7,6 +7,91 @@ import traceback
7
7
  import termios
8
8
  import threading
9
9
  import socket
10
+ import glob
11
+
12
+ PICOTOOL_WRAPPER_CONTENT = """#!/usr/bin/env python3
13
+ import sys
14
+ import os
15
+ import socket
16
+
17
+ def main():
18
+ args = sys.argv[1:]
19
+ target_file = None
20
+ for arg in args:
21
+ if arg.endswith('.elf') or arg.endswith('.uf2'):
22
+ target_file = arg
23
+ break
24
+
25
+ if not target_file:
26
+ sys.exit(0)
27
+
28
+ if target_file.endswith('.elf'):
29
+ uf2_file = target_file[:-4] + '.uf2'
30
+ if not os.path.exists(uf2_file):
31
+ uf2_file = target_file
32
+ else:
33
+ uf2_file = target_file
34
+
35
+ if not os.path.exists(uf2_file):
36
+ print(f"com2tty UF2 wrapper: {uf2_file} not found.", file=sys.stderr)
37
+ sys.exit(1)
38
+
39
+ print(f"com2tty UF2 wrapper: Sending {uf2_file} to host...", file=sys.stderr)
40
+ try:
41
+ with open(uf2_file, 'rb') as f:
42
+ data = f.read()
43
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
44
+ s.connect(('127.0.0.1', {port}))
45
+ s.sendall(data)
46
+ s.close()
47
+ print("com2tty UF2 wrapper: Transfer complete.", file=sys.stderr)
48
+ except Exception as e:
49
+ print(f"com2tty UF2 wrapper error: {e}", file=sys.stderr)
50
+ sys.exit(1)
51
+
52
+ if __name__ == '__main__':
53
+ main()
54
+ """
55
+
56
+ intercepted_picotools = []
57
+
58
+ def setup_picotool_interceptor(uf2_port):
59
+ wrapper_path = "/tmp/com2tty_picotool.py"
60
+ try:
61
+ with open(wrapper_path, "w") as f:
62
+ f.write(PICOTOOL_WRAPPER_CONTENT.replace("{port}", str(uf2_port)))
63
+ os.chmod(wrapper_path, 0o755)
64
+ except Exception as e:
65
+ sys.stderr.write(f"Warning: Failed to create picotool wrapper: {e}\n")
66
+ return
67
+
68
+ home = os.path.expanduser("~")
69
+ search_pattern = os.path.join(home, ".platformio", "packages", "tool-picotool*", "picotool")
70
+ for picotool_path in glob.glob(search_pattern):
71
+ if os.path.islink(picotool_path) or not os.path.isfile(picotool_path):
72
+ continue
73
+ real_path = picotool_path + ".real"
74
+ try:
75
+ if not os.path.exists(real_path):
76
+ os.rename(picotool_path, real_path)
77
+ if os.path.lexists(picotool_path):
78
+ os.remove(picotool_path)
79
+ os.symlink(wrapper_path, picotool_path)
80
+ intercepted_picotools.append((picotool_path, real_path))
81
+ sys.stderr.write(f"Intercepted picotool at {picotool_path}\n")
82
+ except Exception as e:
83
+ sys.stderr.write(f"Warning: Failed to intercept {picotool_path}: {e}\n")
84
+
85
+ def cleanup_picotool_interceptor():
86
+ for picotool_path, real_path in intercepted_picotools:
87
+ try:
88
+ if os.path.lexists(picotool_path):
89
+ os.remove(picotool_path)
90
+ if os.path.exists(real_path):
91
+ os.rename(real_path, picotool_path)
92
+ sys.stderr.write(f"Restored picotool at {picotool_path}\n")
93
+ except Exception as e:
94
+ sys.stderr.write(f"Warning: Failed to restore {picotool_path}: {e}\n")
10
95
 
11
96
  baud_map = {getattr(termios, k): int(k[1:]) for k in dir(termios) if k.startswith('B') and k[1:].isdigit()}
12
97
 
@@ -171,6 +256,104 @@ def run_rfc2217_server_thread(port, rfc2217_active):
171
256
  finally:
172
257
  s.close()
173
258
 
259
+ def run_uf2_relay_thread(port, uf2_active):
260
+ """
261
+ TCP server inside WSL that receives UF2 data from the picotool wrapper
262
+ and relays it to the Windows host through stdout pipe with control messages.
263
+ """
264
+ import subprocess as sp
265
+ import time
266
+
267
+ # Kill any leftover process from a previous com2tty session
268
+ try:
269
+ sp.run(["fuser", "-k", f"{port}/tcp"], capture_output=True, timeout=3)
270
+ time.sleep(0.3)
271
+ except Exception:
272
+ pass
273
+
274
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
275
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
276
+ try:
277
+ s.bind(('127.0.0.1', port))
278
+ except Exception as e:
279
+ sys.stderr.write(f"[CONTROL] UF2_ERROR: bind failed on port {port}: {e}\n")
280
+ sys.stderr.flush()
281
+ return
282
+ s.listen(1)
283
+ s.settimeout(1.0)
284
+
285
+ sys.stderr.write(f"[CONTROL] UF2_READY:{port}\n")
286
+ sys.stderr.flush()
287
+
288
+ try:
289
+ while True:
290
+ try:
291
+ conn, addr = s.accept()
292
+ except socket.timeout:
293
+ continue
294
+ except Exception:
295
+ break
296
+
297
+ # Read all UF2 data from the picotool wrapper
298
+ uf2_data = bytearray()
299
+ try:
300
+ while True:
301
+ chunk = conn.recv(65536)
302
+ if not chunk:
303
+ break
304
+ uf2_data.extend(chunk)
305
+ except Exception:
306
+ pass
307
+ finally:
308
+ conn.close()
309
+
310
+ import hashlib
311
+ md5_hash = hashlib.md5(uf2_data).hexdigest()
312
+
313
+ sys.stderr.write(f"[CONTROL] UF2_UPLOAD_START:{len(uf2_data)}:{md5_hash}\n")
314
+ sys.stderr.flush()
315
+
316
+ # Pause the PTY main loop so we own stdout exclusively
317
+ uf2_active.set()
318
+ time.sleep(0.3)
319
+
320
+ # Block, waiting for [CONTROL] UF2_ACK from stdin (fd 0)
321
+ ack_received = False
322
+ timeout_time = time.time() + 5.0
323
+ buffer = b""
324
+ while time.time() < timeout_time and not ack_received:
325
+ r, _, _ = select.select([0], [], [], 0.1)
326
+ if 0 in r:
327
+ try:
328
+ chunk = os.read(0, 1024)
329
+ if not chunk:
330
+ break
331
+ buffer += chunk
332
+ if b"[CONTROL] UF2_ACK" in buffer:
333
+ ack_received = True
334
+ break
335
+ except Exception:
336
+ break
337
+
338
+ if ack_received:
339
+ # Send UF2 binary data through stdout pipe to Windows host
340
+ try:
341
+ sys.stdout.buffer.write(uf2_data)
342
+ sys.stdout.buffer.flush()
343
+ except Exception as e:
344
+ sys.stderr.write(f"[CONTROL] UF2_ERROR: Failed to write to stdout: {e}\n")
345
+ sys.stderr.flush()
346
+ else:
347
+ sys.stderr.write("[CONTROL] UF2_ERROR: Timeout waiting for host UF2_ACK\n")
348
+ sys.stderr.flush()
349
+
350
+ sys.stderr.write("[CONTROL] UF2_UPLOAD_END\n")
351
+ sys.stderr.flush()
352
+
353
+ uf2_active.clear()
354
+ finally:
355
+ s.close()
356
+
174
357
  def main():
175
358
  parser = argparse.ArgumentParser(description="com2tty WSL Bridge Helper")
176
359
  parser.add_argument(
@@ -197,8 +380,9 @@ def main():
197
380
  if args.rfc2217_port:
198
381
  inject_rc(args.rfc2217_port)
199
382
 
200
- # Event to coordinate stdin/stdout access between PTY bridge and RFC 2217 forwarder
383
+ # Events to coordinate stdin/stdout access between PTY bridge, RFC 2217, and UF2 relay
201
384
  rfc2217_active = threading.Event()
385
+ uf2_active = threading.Event()
202
386
 
203
387
  try:
204
388
  master_fd, slave_fd = os.openpty()
@@ -237,12 +421,20 @@ def main():
237
421
 
238
422
  # Start RFC 2217 server thread if port is specified
239
423
  if args.rfc2217_port:
424
+ uf2_port = args.rfc2217_port + 1
425
+ setup_picotool_interceptor(uf2_port)
240
426
  t_rfc2217 = threading.Thread(
241
427
  target=run_rfc2217_server_thread,
242
428
  args=(args.rfc2217_port, rfc2217_active),
243
429
  daemon=True
244
430
  )
245
431
  t_rfc2217.start()
432
+ t_uf2_relay = threading.Thread(
433
+ target=run_uf2_relay_thread,
434
+ args=(uf2_port, uf2_active),
435
+ daemon=True
436
+ )
437
+ t_uf2_relay.start()
246
438
 
247
439
  # Select loop
248
440
  # 0 is stdin, master_fd is the pseudo-terminal master
@@ -252,8 +444,8 @@ def main():
252
444
  last_settings = None
253
445
 
254
446
  while True:
255
- # Yield stdin/stdout to RFC 2217 forwarder when active
256
- if rfc2217_active.is_set():
447
+ # Yield stdin/stdout to RFC 2217 forwarder or UF2 relay when active
448
+ if rfc2217_active.is_set() or uf2_active.is_set():
257
449
  import time
258
450
  time.sleep(0.1)
259
451
  continue
@@ -268,8 +460,8 @@ def main():
268
460
  # select blocks until data is available on stdin or master_fd
269
461
  r, w, x = select.select([0, master_fd], [], [], 0.5)
270
462
 
271
- # Re-check after select returns (RFC 2217 might have activated during select)
272
- if rfc2217_active.is_set():
463
+ # Re-check after select returns (RFC 2217 or UF2 might have activated during select)
464
+ if rfc2217_active.is_set() or uf2_active.is_set():
273
465
  continue
274
466
 
275
467
  if 0 in r:
@@ -309,6 +501,7 @@ def main():
309
501
  # Clean up symlink and file descriptors
310
502
  if args.rfc2217_port:
311
503
  clean_rc()
504
+ cleanup_picotool_interceptor()
312
505
  if created_symlink:
313
506
  cleanup_symlink(created_symlink)
314
507
  if slave_fd is not None: