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.
Files changed (29) hide show
  1. {com2tty-0.1.0/src/com2tty.egg-info → com2tty-0.1.2}/PKG-INFO +1 -1
  2. {com2tty-0.1.0 → com2tty-0.1.2}/pyproject.toml +1 -1
  3. com2tty-0.1.2/src/com2tty/__init__.py +1 -0
  4. com2tty-0.1.2/src/com2tty/bridge.py +521 -0
  5. {com2tty-0.1.0 → com2tty-0.1.2}/src/com2tty/cli.py +12 -4
  6. com2tty-0.1.2/src/com2tty/host.py +879 -0
  7. com2tty-0.1.2/src/com2tty/rfc2217_server.py +89 -0
  8. {com2tty-0.1.0 → com2tty-0.1.2/src/com2tty.egg-info}/PKG-INFO +1 -1
  9. {com2tty-0.1.0 → com2tty-0.1.2}/src/com2tty.egg-info/SOURCES.txt +3 -1
  10. com2tty-0.1.2/tests/test_bridge_script.py +1070 -0
  11. com2tty-0.1.2/tests/test_cli.py +80 -0
  12. com2tty-0.1.2/tests/test_host.py +1803 -0
  13. com2tty-0.1.2/tests/test_rfc2217_server.py +195 -0
  14. com2tty-0.1.0/src/com2tty/__init__.py +0 -1
  15. com2tty-0.1.0/src/com2tty/bridge.py +0 -131
  16. com2tty-0.1.0/src/com2tty/host.py +0 -180
  17. com2tty-0.1.0/tests/test_bridge_script.py +0 -156
  18. com2tty-0.1.0/tests/test_cli.py +0 -46
  19. com2tty-0.1.0/tests/test_host.py +0 -192
  20. {com2tty-0.1.0 → com2tty-0.1.2}/LICENSE +0 -0
  21. {com2tty-0.1.0 → com2tty-0.1.2}/README.md +0 -0
  22. {com2tty-0.1.0 → com2tty-0.1.2}/setup.cfg +0 -0
  23. {com2tty-0.1.0 → com2tty-0.1.2}/setup.py +0 -0
  24. {com2tty-0.1.0 → com2tty-0.1.2}/src/com2tty/__main__.py +0 -0
  25. {com2tty-0.1.0 → com2tty-0.1.2}/src/com2tty.egg-info/dependency_links.txt +0 -0
  26. {com2tty-0.1.0 → com2tty-0.1.2}/src/com2tty.egg-info/entry_points.txt +0 -0
  27. {com2tty-0.1.0 → com2tty-0.1.2}/src/com2tty.egg-info/requires.txt +0 -0
  28. {com2tty-0.1.0 → com2tty-0.1.2}/src/com2tty.egg-info/top_level.txt +0 -0
  29. {com2tty-0.1.0 → com2tty-0.1.2}/tests/test_main.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: com2tty
3
- Version: 0.1.0
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.0"
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"
@@ -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=int,
19
- default=9600,
20
- help="Baud rate for the serial port (default: 9600)."
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.")