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.
- {com2tty-0.1.1/src/com2tty.egg-info → com2tty-0.1.2}/PKG-INFO +1 -1
- {com2tty-0.1.1 → com2tty-0.1.2}/pyproject.toml +1 -1
- com2tty-0.1.2/src/com2tty/__init__.py +1 -0
- {com2tty-0.1.1 → com2tty-0.1.2}/src/com2tty/bridge.py +198 -5
- com2tty-0.1.2/src/com2tty/host.py +879 -0
- {com2tty-0.1.1 → com2tty-0.1.2/src/com2tty.egg-info}/PKG-INFO +1 -1
- com2tty-0.1.2/tests/test_bridge_script.py +1070 -0
- com2tty-0.1.2/tests/test_host.py +1803 -0
- com2tty-0.1.1/src/com2tty/__init__.py +0 -1
- com2tty-0.1.1/src/com2tty/host.py +0 -424
- com2tty-0.1.1/tests/test_bridge_script.py +0 -564
- com2tty-0.1.1/tests/test_host.py +0 -544
- {com2tty-0.1.1 → com2tty-0.1.2}/LICENSE +0 -0
- {com2tty-0.1.1 → com2tty-0.1.2}/README.md +0 -0
- {com2tty-0.1.1 → com2tty-0.1.2}/setup.cfg +0 -0
- {com2tty-0.1.1 → com2tty-0.1.2}/setup.py +0 -0
- {com2tty-0.1.1 → com2tty-0.1.2}/src/com2tty/__main__.py +0 -0
- {com2tty-0.1.1 → com2tty-0.1.2}/src/com2tty/cli.py +0 -0
- {com2tty-0.1.1 → com2tty-0.1.2}/src/com2tty/rfc2217_server.py +0 -0
- {com2tty-0.1.1 → com2tty-0.1.2}/src/com2tty.egg-info/SOURCES.txt +0 -0
- {com2tty-0.1.1 → com2tty-0.1.2}/src/com2tty.egg-info/dependency_links.txt +0 -0
- {com2tty-0.1.1 → com2tty-0.1.2}/src/com2tty.egg-info/entry_points.txt +0 -0
- {com2tty-0.1.1 → com2tty-0.1.2}/src/com2tty.egg-info/requires.txt +0 -0
- {com2tty-0.1.1 → com2tty-0.1.2}/src/com2tty.egg-info/top_level.txt +0 -0
- {com2tty-0.1.1 → com2tty-0.1.2}/tests/test_cli.py +0 -0
- {com2tty-0.1.1 → com2tty-0.1.2}/tests/test_main.py +0 -0
- {com2tty-0.1.1 → com2tty-0.1.2}/tests/test_rfc2217_server.py +0 -0
|
@@ -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
|
-
#
|
|
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:
|