com2tty 0.1.0__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.0/src/com2tty.egg-info → com2tty-0.1.2}/PKG-INFO +1 -1
- {com2tty-0.1.0 → com2tty-0.1.2}/pyproject.toml +1 -1
- com2tty-0.1.2/src/com2tty/__init__.py +1 -0
- com2tty-0.1.2/src/com2tty/bridge.py +521 -0
- {com2tty-0.1.0 → com2tty-0.1.2}/src/com2tty/cli.py +12 -4
- com2tty-0.1.2/src/com2tty/host.py +879 -0
- com2tty-0.1.2/src/com2tty/rfc2217_server.py +89 -0
- {com2tty-0.1.0 → com2tty-0.1.2/src/com2tty.egg-info}/PKG-INFO +1 -1
- {com2tty-0.1.0 → com2tty-0.1.2}/src/com2tty.egg-info/SOURCES.txt +3 -1
- com2tty-0.1.2/tests/test_bridge_script.py +1070 -0
- com2tty-0.1.2/tests/test_cli.py +80 -0
- com2tty-0.1.2/tests/test_host.py +1803 -0
- com2tty-0.1.2/tests/test_rfc2217_server.py +195 -0
- com2tty-0.1.0/src/com2tty/__init__.py +0 -1
- com2tty-0.1.0/src/com2tty/bridge.py +0 -131
- com2tty-0.1.0/src/com2tty/host.py +0 -180
- com2tty-0.1.0/tests/test_bridge_script.py +0 -156
- com2tty-0.1.0/tests/test_cli.py +0 -46
- com2tty-0.1.0/tests/test_host.py +0 -192
- {com2tty-0.1.0 → com2tty-0.1.2}/LICENSE +0 -0
- {com2tty-0.1.0 → com2tty-0.1.2}/README.md +0 -0
- {com2tty-0.1.0 → com2tty-0.1.2}/setup.cfg +0 -0
- {com2tty-0.1.0 → com2tty-0.1.2}/setup.py +0 -0
- {com2tty-0.1.0 → com2tty-0.1.2}/src/com2tty/__main__.py +0 -0
- {com2tty-0.1.0 → com2tty-0.1.2}/src/com2tty.egg-info/dependency_links.txt +0 -0
- {com2tty-0.1.0 → com2tty-0.1.2}/src/com2tty.egg-info/entry_points.txt +0 -0
- {com2tty-0.1.0 → com2tty-0.1.2}/src/com2tty.egg-info/requires.txt +0 -0
- {com2tty-0.1.0 → com2tty-0.1.2}/src/com2tty.egg-info/top_level.txt +0 -0
- {com2tty-0.1.0 → com2tty-0.1.2}/tests/test_main.py +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.2"
|
|
@@ -0,0 +1,521 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import os
|
|
3
|
+
import select
|
|
4
|
+
import argparse
|
|
5
|
+
import signal
|
|
6
|
+
import traceback
|
|
7
|
+
import termios
|
|
8
|
+
import threading
|
|
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")
|
|
95
|
+
|
|
96
|
+
baud_map = {getattr(termios, k): int(k[1:]) for k in dir(termios) if k.startswith('B') and k[1:].isdigit()}
|
|
97
|
+
|
|
98
|
+
MARKER_START = "# === COM2TTY INJECTION START ==="
|
|
99
|
+
MARKER_END = "# === COM2TTY INJECTION END ==="
|
|
100
|
+
|
|
101
|
+
def get_rc_files():
|
|
102
|
+
home = os.path.expanduser("~")
|
|
103
|
+
return [os.path.join(home, ".bashrc")]
|
|
104
|
+
|
|
105
|
+
def clean_rc():
|
|
106
|
+
for rc_path in get_rc_files():
|
|
107
|
+
if not os.path.exists(rc_path):
|
|
108
|
+
continue
|
|
109
|
+
try:
|
|
110
|
+
with open(rc_path, "r") as f:
|
|
111
|
+
lines = f.readlines()
|
|
112
|
+
new_lines = []
|
|
113
|
+
in_block = False
|
|
114
|
+
for line in lines:
|
|
115
|
+
if MARKER_START in line:
|
|
116
|
+
in_block = True
|
|
117
|
+
continue
|
|
118
|
+
if MARKER_END in line:
|
|
119
|
+
in_block = False
|
|
120
|
+
continue
|
|
121
|
+
if not in_block:
|
|
122
|
+
new_lines.append(line)
|
|
123
|
+
with open(rc_path, "w") as f:
|
|
124
|
+
f.writelines(new_lines)
|
|
125
|
+
sys.stderr.write(f"Cleaned injection from {rc_path}\n")
|
|
126
|
+
sys.stderr.flush()
|
|
127
|
+
except Exception as e:
|
|
128
|
+
sys.stderr.write(f"Warning: could not clean {rc_path}: {e}\n")
|
|
129
|
+
sys.stderr.flush()
|
|
130
|
+
|
|
131
|
+
def inject_rc(port):
|
|
132
|
+
clean_rc()
|
|
133
|
+
block = (
|
|
134
|
+
f"{MARKER_START}\n"
|
|
135
|
+
f"export PLATFORMIO_UPLOAD_PORT=rfc2217://127.0.0.1:{port}\n"
|
|
136
|
+
f"export PLATFORMIO_MONITOR_PORT=/tmp/ttyUSB0\n"
|
|
137
|
+
f"{MARKER_END}\n"
|
|
138
|
+
)
|
|
139
|
+
for rc_path in get_rc_files():
|
|
140
|
+
try:
|
|
141
|
+
with open(rc_path, "a") as f:
|
|
142
|
+
f.write(block)
|
|
143
|
+
sys.stderr.write(f"Injected environment variables to {rc_path}\n")
|
|
144
|
+
sys.stderr.flush()
|
|
145
|
+
except Exception as e:
|
|
146
|
+
sys.stderr.write(f"Warning: could not inject into {rc_path}: {e}\n")
|
|
147
|
+
sys.stderr.flush()
|
|
148
|
+
|
|
149
|
+
def get_pty_settings(fd):
|
|
150
|
+
try:
|
|
151
|
+
attrs = termios.tcgetattr(fd)
|
|
152
|
+
speed = attrs[5]
|
|
153
|
+
baud = baud_map.get(speed)
|
|
154
|
+
cflag = attrs[2]
|
|
155
|
+
cs_mask = termios.CS5 | termios.CS6 | termios.CS7 | termios.CS8
|
|
156
|
+
cs_val = cflag & cs_mask
|
|
157
|
+
bytesize_map = {termios.CS5: 5, termios.CS6: 6, termios.CS7: 7, termios.CS8: 8}
|
|
158
|
+
bytesize = bytesize_map.get(cs_val, 8)
|
|
159
|
+
if cflag & termios.PARENB:
|
|
160
|
+
parity = 'O' if (cflag & termios.PARODD) else 'E'
|
|
161
|
+
else:
|
|
162
|
+
parity = 'N'
|
|
163
|
+
stopbits = '2' if (cflag & termios.CSTOPB) else '1'
|
|
164
|
+
return baud, bytesize, parity, stopbits
|
|
165
|
+
except Exception:
|
|
166
|
+
return None, None, None, None
|
|
167
|
+
|
|
168
|
+
def cleanup_symlink(path):
|
|
169
|
+
try:
|
|
170
|
+
if os.path.lexists(path):
|
|
171
|
+
os.unlink(path)
|
|
172
|
+
sys.stderr.write(f"Removed symlink {path}\n")
|
|
173
|
+
sys.stderr.flush()
|
|
174
|
+
except Exception as e:
|
|
175
|
+
sys.stderr.write(f"Warning: Failed to remove symlink {path}: {e}\n")
|
|
176
|
+
sys.stderr.flush()
|
|
177
|
+
|
|
178
|
+
def run_rfc2217_server_thread(port, rfc2217_active):
|
|
179
|
+
"""
|
|
180
|
+
Long-lived TCP forwarder that runs as a thread inside the main bridge process.
|
|
181
|
+
Accepts esptool connections and relays data through stdin/stdout (shared with
|
|
182
|
+
the PTY bridge, coordinated by the rfc2217_active event).
|
|
183
|
+
"""
|
|
184
|
+
import subprocess as sp
|
|
185
|
+
|
|
186
|
+
# Kill any leftover process from a previous com2tty session
|
|
187
|
+
try:
|
|
188
|
+
sp.run(["fuser", "-k", f"{port}/tcp"], capture_output=True, timeout=3)
|
|
189
|
+
import time
|
|
190
|
+
time.sleep(0.3)
|
|
191
|
+
except Exception:
|
|
192
|
+
pass
|
|
193
|
+
|
|
194
|
+
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
195
|
+
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
196
|
+
try:
|
|
197
|
+
s.bind(('127.0.0.1', port))
|
|
198
|
+
except Exception as e:
|
|
199
|
+
sys.stderr.write(f"[CONTROL] RFC2217_ERROR: bind failed: {e}\n")
|
|
200
|
+
sys.stderr.flush()
|
|
201
|
+
return
|
|
202
|
+
s.listen(1)
|
|
203
|
+
s.settimeout(1.0)
|
|
204
|
+
|
|
205
|
+
sys.stderr.write(f"[CONTROL] RFC2217_READY:{port}\n")
|
|
206
|
+
sys.stderr.flush()
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
while True:
|
|
210
|
+
try:
|
|
211
|
+
conn, addr = s.accept()
|
|
212
|
+
except socket.timeout:
|
|
213
|
+
continue
|
|
214
|
+
except Exception:
|
|
215
|
+
break
|
|
216
|
+
|
|
217
|
+
conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
|
218
|
+
|
|
219
|
+
# Signal connection to Windows side and pause PTY bridge
|
|
220
|
+
sys.stderr.write("[CONTROL] RFC2217_CONNECT\n")
|
|
221
|
+
sys.stderr.flush()
|
|
222
|
+
rfc2217_active.set()
|
|
223
|
+
import time
|
|
224
|
+
time.sleep(0.3) # Wait for main loop to yield stdin/stdout
|
|
225
|
+
|
|
226
|
+
conn.setblocking(False)
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
while True:
|
|
230
|
+
r, _, _ = select.select([0, conn], [], [], 0.5)
|
|
231
|
+
if 0 in r:
|
|
232
|
+
data = os.read(0, 4096)
|
|
233
|
+
if not data:
|
|
234
|
+
break
|
|
235
|
+
conn.sendall(data)
|
|
236
|
+
if conn in r:
|
|
237
|
+
try:
|
|
238
|
+
data = conn.recv(4096)
|
|
239
|
+
if not data:
|
|
240
|
+
break
|
|
241
|
+
os.write(1, data)
|
|
242
|
+
except BlockingIOError:
|
|
243
|
+
continue
|
|
244
|
+
except ConnectionResetError:
|
|
245
|
+
break
|
|
246
|
+
except Exception as e:
|
|
247
|
+
sys.stderr.write(f"[CONTROL] RFC2217_ERROR: session: {e}\n")
|
|
248
|
+
sys.stderr.flush()
|
|
249
|
+
finally:
|
|
250
|
+
conn.close()
|
|
251
|
+
|
|
252
|
+
# Signal disconnection and resume PTY bridge
|
|
253
|
+
rfc2217_active.clear()
|
|
254
|
+
sys.stderr.write("[CONTROL] RFC2217_DISCONNECT\n")
|
|
255
|
+
sys.stderr.flush()
|
|
256
|
+
finally:
|
|
257
|
+
s.close()
|
|
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
|
+
|
|
357
|
+
def main():
|
|
358
|
+
parser = argparse.ArgumentParser(description="com2tty WSL Bridge Helper")
|
|
359
|
+
parser.add_argument(
|
|
360
|
+
"-s", "--symlink",
|
|
361
|
+
required=True,
|
|
362
|
+
help="Target symlink path for the pseudo-terminal device."
|
|
363
|
+
)
|
|
364
|
+
parser.add_argument(
|
|
365
|
+
"-r", "--rfc2217-port",
|
|
366
|
+
type=int,
|
|
367
|
+
help="TCP port for RFC 2217 server to inject into bashrc and listen on"
|
|
368
|
+
)
|
|
369
|
+
args = parser.parse_args()
|
|
370
|
+
|
|
371
|
+
target_path = args.symlink
|
|
372
|
+
created_symlink = None
|
|
373
|
+
|
|
374
|
+
# We must keep both master and slave descriptors open.
|
|
375
|
+
# Keeping slave_fd open prevents EIO errors on the master side when
|
|
376
|
+
# WSL clients open and close the virtual serial port.
|
|
377
|
+
master_fd = None
|
|
378
|
+
slave_fd = None
|
|
379
|
+
|
|
380
|
+
if args.rfc2217_port:
|
|
381
|
+
inject_rc(args.rfc2217_port)
|
|
382
|
+
|
|
383
|
+
# Events to coordinate stdin/stdout access between PTY bridge, RFC 2217, and UF2 relay
|
|
384
|
+
rfc2217_active = threading.Event()
|
|
385
|
+
uf2_active = threading.Event()
|
|
386
|
+
|
|
387
|
+
try:
|
|
388
|
+
master_fd, slave_fd = os.openpty()
|
|
389
|
+
slave_name = os.ttyname(slave_fd)
|
|
390
|
+
|
|
391
|
+
sys.stderr.write(f"Created pseudo-terminal: master_fd={master_fd}, slave={slave_name}\n")
|
|
392
|
+
sys.stderr.flush()
|
|
393
|
+
|
|
394
|
+
# Attempt to create the symlink at target path
|
|
395
|
+
try:
|
|
396
|
+
if os.path.lexists(target_path):
|
|
397
|
+
os.unlink(target_path)
|
|
398
|
+
os.symlink(slave_name, target_path)
|
|
399
|
+
created_symlink = target_path
|
|
400
|
+
sys.stderr.write(f"Successfully symlinked {target_path} -> {slave_name}\n")
|
|
401
|
+
sys.stderr.flush()
|
|
402
|
+
except PermissionError:
|
|
403
|
+
# Fallback to /tmp if write permission to dev is denied
|
|
404
|
+
basename = os.path.basename(target_path)
|
|
405
|
+
fallback_path = f"/tmp/{basename}"
|
|
406
|
+
sys.stderr.write(f"Warning: Permission denied creating symlink at {target_path}.\n")
|
|
407
|
+
sys.stderr.write(f"Attempting fallback to user-writable path: {fallback_path}...\n")
|
|
408
|
+
sys.stderr.flush()
|
|
409
|
+
|
|
410
|
+
if os.path.lexists(fallback_path):
|
|
411
|
+
os.unlink(fallback_path)
|
|
412
|
+
os.symlink(slave_name, fallback_path)
|
|
413
|
+
created_symlink = fallback_path
|
|
414
|
+
|
|
415
|
+
sys.stderr.write(f"Fallback successful: {fallback_path} -> {slave_name}\n")
|
|
416
|
+
sys.stderr.write("--------------------------------------------------\n")
|
|
417
|
+
sys.stderr.write(f"To use the desired device path '{target_path}', please run this command ONCE in WSL:\n")
|
|
418
|
+
sys.stderr.write(f" sudo ln -sf {fallback_path} {target_path}\n")
|
|
419
|
+
sys.stderr.write("--------------------------------------------------\n")
|
|
420
|
+
sys.stderr.flush()
|
|
421
|
+
|
|
422
|
+
# Start RFC 2217 server thread if port is specified
|
|
423
|
+
if args.rfc2217_port:
|
|
424
|
+
uf2_port = args.rfc2217_port + 1
|
|
425
|
+
setup_picotool_interceptor(uf2_port)
|
|
426
|
+
t_rfc2217 = threading.Thread(
|
|
427
|
+
target=run_rfc2217_server_thread,
|
|
428
|
+
args=(args.rfc2217_port, rfc2217_active),
|
|
429
|
+
daemon=True
|
|
430
|
+
)
|
|
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()
|
|
438
|
+
|
|
439
|
+
# Select loop
|
|
440
|
+
# 0 is stdin, master_fd is the pseudo-terminal master
|
|
441
|
+
sys.stderr.write("WSL bridge enter main loop.\n")
|
|
442
|
+
sys.stderr.flush()
|
|
443
|
+
|
|
444
|
+
last_settings = None
|
|
445
|
+
|
|
446
|
+
while True:
|
|
447
|
+
# Yield stdin/stdout to RFC 2217 forwarder or UF2 relay when active
|
|
448
|
+
if rfc2217_active.is_set() or uf2_active.is_set():
|
|
449
|
+
import time
|
|
450
|
+
time.sleep(0.1)
|
|
451
|
+
continue
|
|
452
|
+
|
|
453
|
+
current_settings = get_pty_settings(master_fd)
|
|
454
|
+
if current_settings != last_settings and current_settings[0] is not None:
|
|
455
|
+
baud, bytesize, parity, stopbits = current_settings
|
|
456
|
+
sys.stderr.write(f"[CONTROL] SETTINGS: baud={baud} bytesize={bytesize} parity={parity} stopbits={stopbits}\n")
|
|
457
|
+
sys.stderr.flush()
|
|
458
|
+
last_settings = current_settings
|
|
459
|
+
|
|
460
|
+
# select blocks until data is available on stdin or master_fd
|
|
461
|
+
r, w, x = select.select([0, master_fd], [], [], 0.5)
|
|
462
|
+
|
|
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():
|
|
465
|
+
continue
|
|
466
|
+
|
|
467
|
+
if 0 in r:
|
|
468
|
+
data = os.read(0, 4096)
|
|
469
|
+
if not data:
|
|
470
|
+
sys.stderr.write("EOF on stdin. Exiting.\n")
|
|
471
|
+
sys.stderr.flush()
|
|
472
|
+
break
|
|
473
|
+
os.write(master_fd, data)
|
|
474
|
+
|
|
475
|
+
if master_fd in r:
|
|
476
|
+
# Read from the virtual serial port
|
|
477
|
+
try:
|
|
478
|
+
data = os.read(master_fd, 4096)
|
|
479
|
+
if not data:
|
|
480
|
+
# Should not happen typically while slave_fd is kept open,
|
|
481
|
+
# but handle it gracefully if it does.
|
|
482
|
+
sys.stderr.write("EOF on PTY master. Exiting.\n")
|
|
483
|
+
sys.stderr.flush()
|
|
484
|
+
break
|
|
485
|
+
os.write(1, data)
|
|
486
|
+
except OSError as e:
|
|
487
|
+
# In case of EIO (Input/output error) when slave closes,
|
|
488
|
+
# just ignore it and continue since we keep slave_fd open.
|
|
489
|
+
if e.errno == 5: # EIO
|
|
490
|
+
continue
|
|
491
|
+
else:
|
|
492
|
+
raise e
|
|
493
|
+
|
|
494
|
+
except KeyboardInterrupt: # pragma: no cover
|
|
495
|
+
sys.stderr.write("WSL bridge interrupted by signal.\n")
|
|
496
|
+
sys.stderr.flush()
|
|
497
|
+
except Exception as e: # pragma: no cover
|
|
498
|
+
sys.stderr.write(f"WSL bridge error: {traceback.format_exc()}\n")
|
|
499
|
+
sys.stderr.flush()
|
|
500
|
+
finally:
|
|
501
|
+
# Clean up symlink and file descriptors
|
|
502
|
+
if args.rfc2217_port:
|
|
503
|
+
clean_rc()
|
|
504
|
+
cleanup_picotool_interceptor()
|
|
505
|
+
if created_symlink:
|
|
506
|
+
cleanup_symlink(created_symlink)
|
|
507
|
+
if slave_fd is not None:
|
|
508
|
+
try:
|
|
509
|
+
os.close(slave_fd)
|
|
510
|
+
except Exception:
|
|
511
|
+
pass
|
|
512
|
+
if master_fd is not None:
|
|
513
|
+
try:
|
|
514
|
+
os.close(master_fd)
|
|
515
|
+
except Exception:
|
|
516
|
+
pass
|
|
517
|
+
sys.stderr.write("WSL bridge shut down.\n")
|
|
518
|
+
sys.stderr.flush()
|
|
519
|
+
|
|
520
|
+
if __name__ == "__main__": # pragma: no cover
|
|
521
|
+
main()
|
|
@@ -15,9 +15,9 @@ def main():
|
|
|
15
15
|
|
|
16
16
|
parser.add_argument(
|
|
17
17
|
"-b", "--baud",
|
|
18
|
-
type=
|
|
19
|
-
default=
|
|
20
|
-
help="Baud rate for the serial port (default:
|
|
18
|
+
type=str,
|
|
19
|
+
default="auto",
|
|
20
|
+
help="Baud rate for the serial port or 'auto' to match Windows (default: auto)."
|
|
21
21
|
)
|
|
22
22
|
|
|
23
23
|
parser.add_argument(
|
|
@@ -26,6 +26,13 @@ def main():
|
|
|
26
26
|
help="Target symlink path inside WSL (default: /tmp/ttyUSB0)."
|
|
27
27
|
)
|
|
28
28
|
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
"--rfc2217-port",
|
|
31
|
+
type=int,
|
|
32
|
+
default=4000,
|
|
33
|
+
help="TCP port for RFC 2217 server (default: 4000)."
|
|
34
|
+
)
|
|
35
|
+
|
|
29
36
|
parser.add_argument(
|
|
30
37
|
"--bytesize",
|
|
31
38
|
type=int,
|
|
@@ -96,7 +103,8 @@ def main():
|
|
|
96
103
|
stopbits=parsed_args.stopbits,
|
|
97
104
|
xonxoff=parsed_args.xonxoff,
|
|
98
105
|
rtscts=parsed_args.rtscts,
|
|
99
|
-
dsrdtr=parsed_args.dsrdtr
|
|
106
|
+
dsrdtr=parsed_args.dsrdtr,
|
|
107
|
+
rfc2217_port=parsed_args.rfc2217_port
|
|
100
108
|
)
|
|
101
109
|
except KeyboardInterrupt:
|
|
102
110
|
logging.info("Interrupted by user. Exiting.")
|