com2tty 0.2.0__tar.gz → 0.3.0__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 (91) hide show
  1. {com2tty-0.2.0/src/com2tty.egg-info → com2tty-0.3.0}/PKG-INFO +131 -49
  2. {com2tty-0.2.0 → com2tty-0.3.0}/README.md +130 -48
  3. {com2tty-0.2.0 → com2tty-0.3.0}/pyproject.toml +4 -1
  4. com2tty-0.3.0/src/com2tty/__init__.py +1 -0
  5. com2tty-0.3.0/src/com2tty/bridge.py +21 -0
  6. com2tty-0.2.0/src/com2tty/cli.py → com2tty-0.3.0/src/com2tty/cli/__init__.py +109 -22
  7. {com2tty-0.2.0/src/com2tty → com2tty-0.3.0/src/com2tty/cli}/profiles.py +11 -1
  8. com2tty-0.3.0/src/com2tty/core/__init__.py +6 -0
  9. com2tty-0.3.0/src/com2tty/core/boards.py +77 -0
  10. com2tty-0.3.0/src/com2tty/core/constants.py +91 -0
  11. com2tty-0.3.0/src/com2tty/core/frames.py +136 -0
  12. com2tty-0.3.0/src/com2tty/core/protocol.py +125 -0
  13. com2tty-0.3.0/src/com2tty/pad_bridge.py +21 -0
  14. com2tty-0.3.0/src/com2tty/windows/__init__.py +6 -0
  15. com2tty-0.2.0/src/com2tty/boards.py → com2tty-0.3.0/src/com2tty/windows/board_reset.py +27 -80
  16. com2tty-0.3.0/src/com2tty/windows/bridge_app.py +402 -0
  17. com2tty-0.3.0/src/com2tty/windows/control_handler.py +482 -0
  18. {com2tty-0.2.0/src/com2tty → com2tty-0.3.0/src/com2tty/windows}/discovery.py +13 -4
  19. com2tty-0.3.0/src/com2tty/windows/doctor.py +206 -0
  20. com2tty-0.3.0/src/com2tty/windows/gamepad_app.py +217 -0
  21. com2tty-0.2.0/src/com2tty/xinput.py → com2tty-0.3.0/src/com2tty/windows/gamepad_host.py +15 -60
  22. com2tty-0.3.0/src/com2tty/windows/os_hacks/__init__.py +12 -0
  23. com2tty-0.2.0/src/com2tty/uf2.py → com2tty-0.3.0/src/com2tty/windows/os_hacks/autoplay.py +11 -80
  24. com2tty-0.3.0/src/com2tty/windows/os_hacks/device_watcher.py +197 -0
  25. com2tty-0.3.0/src/com2tty/windows/os_hacks/explorer.py +125 -0
  26. com2tty-0.2.0/src/com2tty/rfc2217_server.py → com2tty-0.3.0/src/com2tty/windows/rfc2217_redirector.py +29 -0
  27. com2tty-0.3.0/src/com2tty/windows/serial_host.py +304 -0
  28. com2tty-0.3.0/src/com2tty/windows/uf2_flash.py +84 -0
  29. com2tty-0.3.0/src/com2tty/windows/wsl_process.py +131 -0
  30. com2tty-0.3.0/src/com2tty/wsl/__init__.py +14 -0
  31. com2tty-0.3.0/src/com2tty/wsl/assets/picotool_wrapper.py.in +42 -0
  32. com2tty-0.2.0/src/com2tty/pad_bridge.py → com2tty-0.3.0/src/com2tty/wsl/evdev_sink.py +91 -203
  33. com2tty-0.3.0/src/com2tty/wsl/gamepad_app.py +99 -0
  34. com2tty-0.3.0/src/com2tty/wsl/integrations/__init__.py +9 -0
  35. com2tty-0.3.0/src/com2tty/wsl/integrations/picotool.py +133 -0
  36. com2tty-0.3.0/src/com2tty/wsl/integrations/shell_env.py +193 -0
  37. com2tty-0.3.0/src/com2tty/wsl/liveness.py +67 -0
  38. com2tty-0.3.0/src/com2tty/wsl/pty_manager.py +100 -0
  39. com2tty-0.3.0/src/com2tty/wsl/serial_app.py +208 -0
  40. com2tty-0.3.0/src/com2tty/wsl/servers/__init__.py +8 -0
  41. com2tty-0.3.0/src/com2tty/wsl/servers/base.py +144 -0
  42. com2tty-0.3.0/src/com2tty/wsl/servers/rfc2217_forwarder.py +85 -0
  43. com2tty-0.3.0/src/com2tty/wsl/servers/uf2_relay.py +117 -0
  44. {com2tty-0.2.0 → com2tty-0.3.0/src/com2tty.egg-info}/PKG-INFO +131 -49
  45. com2tty-0.3.0/src/com2tty.egg-info/SOURCES.txt +83 -0
  46. com2tty-0.3.0/tests/test_autoplay.py +267 -0
  47. com2tty-0.3.0/tests/test_board_reset.py +71 -0
  48. {com2tty-0.2.0 → com2tty-0.3.0}/tests/test_boards.py +19 -17
  49. com2tty-0.3.0/tests/test_bridge_app.py +821 -0
  50. {com2tty-0.2.0 → com2tty-0.3.0}/tests/test_cli.py +95 -8
  51. com2tty-0.3.0/tests/test_console.py +79 -0
  52. com2tty-0.3.0/tests/test_control_handler.py +1151 -0
  53. com2tty-0.3.0/tests/test_core_frames.py +109 -0
  54. com2tty-0.3.0/tests/test_core_protocol.py +129 -0
  55. com2tty-0.3.0/tests/test_devnotify.py +237 -0
  56. {com2tty-0.2.0 → com2tty-0.3.0}/tests/test_discovery.py +25 -2
  57. com2tty-0.3.0/tests/test_doctor.py +323 -0
  58. com2tty-0.3.0/tests/test_entry_shims.py +80 -0
  59. com2tty-0.2.0/tests/test_pad_bridge.py → com2tty-0.3.0/tests/test_evdev_sink.py +180 -89
  60. com2tty-0.3.0/tests/test_gamepad_app.py +331 -0
  61. com2tty-0.3.0/tests/test_liveness.py +117 -0
  62. com2tty-0.3.0/tests/test_picotool.py +333 -0
  63. {com2tty-0.2.0 → com2tty-0.3.0}/tests/test_profiles.py +30 -5
  64. com2tty-0.3.0/tests/test_pty_manager.py +73 -0
  65. com2tty-0.3.0/tests/test_rfc2217_forwarder.py +173 -0
  66. com2tty-0.2.0/tests/test_rfc2217_server.py → com2tty-0.3.0/tests/test_rfc2217_redirector.py +64 -3
  67. com2tty-0.3.0/tests/test_serial_app.py +452 -0
  68. com2tty-0.3.0/tests/test_serial_host.py +493 -0
  69. com2tty-0.3.0/tests/test_shell_env.py +426 -0
  70. com2tty-0.3.0/tests/test_uf2_flash.py +108 -0
  71. com2tty-0.3.0/tests/test_uf2_relay.py +366 -0
  72. com2tty-0.3.0/tests/test_wsl_gamepad_app.py +51 -0
  73. com2tty-0.3.0/tests/test_wsl_process.py +110 -0
  74. com2tty-0.3.0/tests/test_wsl_servers_base.py +103 -0
  75. {com2tty-0.2.0 → com2tty-0.3.0}/tests/test_xinput.py +10 -10
  76. com2tty-0.2.0/src/com2tty/__init__.py +0 -1
  77. com2tty-0.2.0/src/com2tty/bridge.py +0 -666
  78. com2tty-0.2.0/src/com2tty/host.py +0 -1166
  79. com2tty-0.2.0/src/com2tty.egg-info/SOURCES.txt +0 -33
  80. com2tty-0.2.0/tests/test_bridge_script.py +0 -1572
  81. com2tty-0.2.0/tests/test_host.py +0 -3021
  82. {com2tty-0.2.0 → com2tty-0.3.0}/LICENSE +0 -0
  83. {com2tty-0.2.0 → com2tty-0.3.0}/setup.cfg +0 -0
  84. {com2tty-0.2.0 → com2tty-0.3.0}/setup.py +0 -0
  85. {com2tty-0.2.0 → com2tty-0.3.0}/src/com2tty/__main__.py +0 -0
  86. /com2tty-0.2.0/src/com2tty/banner.py → /com2tty-0.3.0/src/com2tty/windows/os_hacks/console.py +0 -0
  87. {com2tty-0.2.0 → com2tty-0.3.0}/src/com2tty.egg-info/dependency_links.txt +0 -0
  88. {com2tty-0.2.0 → com2tty-0.3.0}/src/com2tty.egg-info/entry_points.txt +0 -0
  89. {com2tty-0.2.0 → com2tty-0.3.0}/src/com2tty.egg-info/requires.txt +0 -0
  90. {com2tty-0.2.0 → com2tty-0.3.0}/src/com2tty.egg-info/top_level.txt +0 -0
  91. {com2tty-0.2.0 → com2tty-0.3.0}/tests/test_main.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: com2tty
3
- Version: 0.2.0
3
+ Version: 0.3.0
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
@@ -117,6 +117,19 @@ The following options apply to both modes.
117
117
  -l, --list List the serial ports Windows can see (device name,
118
118
  VID:PID, USB bus id, serial number, detected board,
119
119
  description) and exit.
120
+ --json With --list: print the port list as a JSON array
121
+ instead of an aligned table, for scripts and IDE
122
+ integrations.
123
+ --doctor Run an environment self-check (WSL, python3, drive
124
+ automounting, fuser, TCP port availability, leftovers
125
+ from crashed sessions, /dev/uinput, the XInput DLL)
126
+ and exit. Exit status 1 when a required check fails.
127
+ --auto-respawn Rebuild the bridge automatically when the WSL helper
128
+ dies, for example after `wsl --shutdown` or a WSL
129
+ servicing update: com2tty waits until WSL answers
130
+ again and re-creates the same endpoint. In serial
131
+ mode this implies --wait. Applies to serial and
132
+ gamepad mode alike.
120
133
  -d, --debug Enable verbose debug logging on standard error.
121
134
  --distro NAME WSL distribution to use (default: the WSL default
122
135
  distribution). Useful when the default distribution
@@ -138,6 +151,10 @@ port [port ...] Windows COM port(s) to bridge, for example COM3, or
138
151
  --rfc2217-port PORT TCP port for the in-WSL RFC 2217 forwarder
139
152
  (default: 4000). The UF2 relay uses PORT + 1; with
140
153
  multiple ports, each additional port uses PORT + 2i.
154
+ --wait If the COM port is not present yet, wait for it to
155
+ appear instead of failing, then bridge it. Useful
156
+ when com2tty is started before the device is
157
+ plugged in.
141
158
  --bytesize {5,6,7,8} Serial byte size (default: 8).
142
159
  --parity {N,E,O,S,M} Parity: none, even, odd, space, or mark (default: N).
143
160
  --stopbits {1,1.5,2} Stop bits (default: 1).
@@ -155,7 +172,12 @@ port [port ...] Windows COM port(s) to bridge, for example COM3, or
155
172
 
156
173
  ```text
157
174
  --gamepad Select gamepad mode. No COM port is required.
158
- --pad-index {0,1,2,3} XInput controller slot to forward (default: 0).
175
+ --pad-index {0,1,2,3} [...]
176
+ XInput controller slot(s) to forward (default: 0).
177
+ Several slots may be given (--pad-index 0 1) to
178
+ forward multiple controllers at once; each gets its
179
+ own WSL helper and endpoint (/tmp/com2pad0,
180
+ /tmp/com2pad1, ...).
159
181
  --pad-name NAME Device name advertised inside WSL
160
182
  (default: "Microsoft X-Box 360 pad").
161
183
  --uinput Create a real /dev/input device through /dev/uinput
@@ -218,7 +240,15 @@ stale Windows handle and waits for the device to come back, first under its
218
240
  original COM name and then by scanning for its USB serial number, because
219
241
  Windows may assign a different COM number after a replug. Once the device
220
242
  reappears the bridge resumes automatically; the WSL endpoint stays in place
221
- the whole time.
243
+ the whole time. The waiting loops are event-driven: com2tty registers for
244
+ Windows device-change notifications (`WM_DEVICECHANGE`), so a replugged
245
+ device resumes the moment Windows enumerates it rather than on the next
246
+ polling tick (plain polling remains as the fallback).
247
+
248
+ The complementary case — WSL itself going away, for example through
249
+ `wsl --shutdown` or a WSL update — is covered by `--auto-respawn`: instead
250
+ of exiting when the WSL helper dies, com2tty waits until WSL answers again
251
+ and rebuilds the bridge with the same endpoint paths.
222
252
 
223
253
  ### Bridging multiple ports
224
254
 
@@ -269,9 +299,11 @@ com2tty @pad
269
299
  ### Automatic baud-rate detection
270
300
 
271
301
  When the baud rate is left at its default value of `auto`, com2tty queries the
272
- rate that Windows has configured for the port and uses it. If detection fails,
273
- the bridge falls back to 9600 baud. To set the rate explicitly, pass a numeric
274
- value to `--baud`.
302
+ rate that Windows has configured for the port and uses it. The rate is read
303
+ directly from the Win32 `GetCommState` API, which works regardless of the
304
+ Windows display language; parsing the `mode.com` output is kept only as a
305
+ fallback. If detection fails, the bridge falls back to 9600 baud. To set the
306
+ rate explicitly, pass a numeric value to `--baud`.
275
307
 
276
308
  ```cmd
277
309
  com2tty COM3 --baud auto
@@ -359,7 +391,13 @@ these ports during an upload. Run com2tty only on hosts you trust, and choose a
359
391
  non-default `--rfc2217-port` if another local service needs the default port. To
360
392
  reclaim a port left open by a previous com2tty session, the helper only
361
393
  terminates processes whose command line identifies them as a com2tty bridge; an
362
- unrelated service occupying the port is never killed.
394
+ unrelated service occupying the port is never killed. A *running* com2tty
395
+ session is never killed either: each session refreshes a heartbeat marker for
396
+ its ports, so a second invocation that reuses the same `--rfc2217-port` reports
397
+ the conflict and leaves the first bridge intact. Two sessions can run
398
+ concurrently by giving the second one a different `--rfc2217-port` and
399
+ `--wsl-tty`; each session removes only its own block from the shell startup
400
+ files when it exits.
363
401
 
364
402
  ### Gamepad mode
365
403
 
@@ -396,6 +434,24 @@ interprets them using the device profile below. This tier is suited to programs
396
434
  that read the stream directly. Standard applications and game engines that
397
435
  enumerate `/dev/input` devices do not read a FIFO and require the uinput tier.
398
436
 
437
+ Force feedback is available in this tier through a second FIFO created at
438
+ `<path>.ff` (by default `/tmp/com2pad0.ff`): the consumer writes 6-byte rumble
439
+ frames into it — the bytes `0xFB 0xFE` followed by the strong (left,
440
+ low-frequency) and weak (right, high-frequency) motor magnitudes as two
441
+ little-endian unsigned 16-bit values — and com2tty forwards them to the
442
+ physical controller's motors, exactly as the uinput tier does for kernel
443
+ `FF_RUMBLE` effects.
444
+
445
+ To forward several controllers at once, pass several slots:
446
+
447
+ ```cmd
448
+ com2tty --gamepad --pad-index 0 1
449
+ ```
450
+
451
+ Each slot gets its own WSL helper and its own endpoint (`/tmp/com2pad0`,
452
+ `/tmp/com2pad1`, ...); in the uinput tier each helper creates its own
453
+ `/dev/input` device, as if several physical controllers were attached.
454
+
399
455
  #### Opt-in tier: a real device through /dev/uinput
400
456
 
401
457
  The opt-in tier creates a real system-wide device under `/dev/input` so that SDL2
@@ -472,8 +528,9 @@ feedback. When a game or emulator inside WSL plays a rumble effect, the
472
528
  effect's magnitudes travel back through the bridge to the Windows host, which
473
529
  drives the physical controller's motors through `XInputSetState`. The strong
474
530
  (left, low-frequency) and weak (right, high-frequency) motors map directly to
475
- their XInput counterparts. The `/tmp` stream tier has no reverse channel and
476
- therefore no force feedback.
531
+ their XInput counterparts. In the `/tmp` stream tier the same reverse channel
532
+ is reached by writing rumble frames into the `<path>.ff` FIFO, as described
533
+ in [the default tier](#default-tier-the-tmp-event-stream).
477
534
 
478
535
  The forwarded signal matches a real controller at the level of these event codes,
479
536
  ranges, and resolutions, but it is not bit-for-bit identical to a controller
@@ -487,48 +544,68 @@ available. These differences are inherent to the approach.
487
544
 
488
545
  The package is organised around a host process on Windows and a helper process
489
546
  inside WSL connected by the standard input and output streams of the helper.
547
+ The code under `src/com2tty/` is split by where it runs: `cli/` is the
548
+ command-line layer, `core/` holds the dependency-free definitions both sides
549
+ share, `windows/` runs on the Windows interpreter, and `wsl/` runs on the
550
+ Linux interpreter inside WSL.
490
551
 
491
- `cli.py` parses the command line (after `profiles.py` expands any `@profile`
492
- tokens) and dispatches to an entry function in `host.py`: `run_bridge` in
552
+ `cli/` parses the command line (after `cli/profiles.py` expands any `@profile`
553
+ tokens) and dispatches to an entry function in `windows/`: `run_bridge` in
493
554
  serial mode, `run_multi_bridge` when several ports are given, and
494
- `run_gamepad_bridge` in gamepad mode. `discovery.py` implements `--list`.
495
- `__main__.py` and the console entry point both call `cli.main`, and
496
- `__init__.py` holds the package version.
497
-
498
- `host.py` is the Windows side. In serial mode `run_bridge` opens the COM port with
499
- `pyserial`, spawns the WSL helper with `wsl python3 -u bridge.py`, and runs three
500
- threads: one relays bytes from the COM port to the helper's standard input, one
501
- relays bytes from the helper's standard output to the COM port, and one reads the
502
- helper's standard error. The standard error stream carries a line-oriented control
503
- protocol whose messages are prefixed with `[CONTROL]`; these messages drive
504
- dynamic serial-setting changes, the RFC 2217 session lifecycle, and the UF2 upload
505
- sequence. `host.py` also contains the hot-plug reconnect logic and the routine
506
- that writes a transferred UF2 image to the correct Windows drive. The board
507
- detection and reset sequences live in `boards.py`, the UF2 drive lookup and
508
- AutoPlay suppression in `uf2.py`, and the console colour handling in
509
- `banner.py`; `host.py` re-exports these names for backwards compatibility.
510
-
511
- `bridge.py` is the WSL side for serial forwarding. It creates a pseudo terminal
512
- with `openpty`, symlinks the requested path to the pseudo-terminal slave, falling
513
- back to `/tmp` if the requested path is not writable, and runs a `select` loop
514
- that relays data between the helper's standard input and output and the
515
- pseudo-terminal master. It also starts the RFC 2217 forwarder thread and the UF2
516
- relay thread, writes the PlatformIO environment variables into `~/.bashrc`, and
517
- installs the `picotool` interceptor. `rfc2217_server.py` provides the redirector
518
- that implements the RFC 2217 protocol for the forwarder.
519
-
520
- The gamepad path reuses the same spawn-and-pipe transport. `xinput.py` is the
521
- Windows side: it polls an XInput controller slot through `ctypes` (preferring
522
- the `XInputGetStateEx` export so the Guide button is visible) and packs each
523
- state snapshot into a fixed 16-byte frame, sending a frame only when the state
524
- changes. `pad_bridge.py` is the WSL side: it parses the frames, translates them
525
- into evdev events, and writes them to one of two sinks. The default sink writes to
526
- a `/tmp` FIFO, and the opt-in sink creates a real device through `/dev/uinput`
527
- using raw `ioctl` calls. Both sinks share the same event-encoding code, so the
528
- byte stream they produce is identical. In the uinput sink the helper also
529
- services the kernel's force-feedback upload handshake and streams played
530
- rumble effects back over its stdout, where the host applies them to the
531
- physical controller with `XInputSetState`.
555
+ `run_gamepad_bridge` in gamepad mode. `windows/discovery.py` implements
556
+ `--list` and `windows/doctor.py` implements `--doctor`. `__main__.py` and the
557
+ console entry point both call `cli.main`, and `__init__.py` holds the package
558
+ version.
559
+
560
+ `core/` defines the contracts both interpreters rely on: `core/protocol.py`
561
+ holds the `[CONTROL]` message catalogue and the dispatcher the host routes
562
+ stderr lines through, `core/frames.py` the binary gamepad frame codecs,
563
+ `core/boards.py` the USB VID classification with the reset timing data, and
564
+ `core/constants.py` the shared paths, ports, and marker strings.
565
+
566
+ `windows/` is the Windows side. In serial mode `run_bridge` in
567
+ `windows/bridge_app.py` opens the COM port with `pyserial`, spawns the WSL
568
+ helper with `wsl python3 -u bridge.py` (through `windows/wsl_process.py`), and
569
+ runs three threads: one relays bytes from the COM port to the helper's
570
+ standard input, one relays bytes from the helper's standard output to the COM
571
+ port, and one reads the helper's standard error. The standard error stream
572
+ carries a line-oriented control protocol whose messages are prefixed with
573
+ `[CONTROL]`; the handlers in `windows/control_handler.py` drive dynamic
574
+ serial-setting changes, the RFC 2217 session lifecycle, and the UF2 upload
575
+ sequence (including the routine that writes a transferred UF2 image to the
576
+ correct Windows drive, via `windows/uf2_flash.py`). The hot-plug reconnect
577
+ logic lives in `windows/serial_host.py` and the board reset sequences in
578
+ `windows/board_reset.py`. The raw OS-level interventions -- AutoPlay
579
+ suppression, Explorer window closing, `WM_DEVICECHANGE` wake-ups, and console
580
+ VT mode -- are isolated under `windows/os_hacks/`.
581
+
582
+ `wsl/` is the WSL side, restricted to the Python standard library.
583
+ `wsl/serial_app.py` (launched through the `bridge.py` shim at the package
584
+ root) creates a pseudo terminal with `openpty` via `wsl/pty_manager.py`,
585
+ symlinks the requested path to the pseudo-terminal slave, falling back to
586
+ `/tmp` if the requested path is not writable, and runs a `select` loop that
587
+ relays data between the helper's standard input and output and the
588
+ pseudo-terminal master. It also starts the RFC 2217 forwarder and UF2 relay
589
+ threads (`wsl/servers/`), writes the PlatformIO environment variables into
590
+ `~/.bashrc` (`wsl/integrations/shell_env.py`), and installs the `picotool`
591
+ interceptor (`wsl/integrations/picotool.py`).
592
+ `windows/rfc2217_redirector.py` provides the redirector that implements the
593
+ RFC 2217 protocol for the forwarder.
594
+
595
+ The gamepad path reuses the same spawn-and-pipe transport.
596
+ `windows/gamepad_host.py` is the Windows side: it polls an XInput controller
597
+ slot through `ctypes` (preferring the `XInputGetStateEx` export so the Guide
598
+ button is visible) and packs each state snapshot into a fixed 16-byte frame,
599
+ sending a frame only when the state changes. `wsl/gamepad_app.py` (launched
600
+ through the `pad_bridge.py` shim) is the WSL side: it parses the frames,
601
+ translates them into evdev events, and writes them to one of the two sinks in
602
+ `wsl/evdev_sink.py`. The default sink writes to a `/tmp` FIFO, and the opt-in
603
+ sink creates a real device through `/dev/uinput` using raw `ioctl` calls. Both
604
+ sinks share the same event-encoding code, so the byte stream they produce is
605
+ identical. In the uinput sink the helper also services the kernel's
606
+ force-feedback upload handshake and streams played rumble effects back over
607
+ its stdout, where the host applies them to the physical controller with
608
+ `XInputSetState`.
532
609
 
533
610
  For a detailed account of the control protocol, the board reset sequences, the
534
611
  reconnection model, the binary frame formats, and the known hardware-unverified
@@ -588,6 +665,11 @@ step, is documented in [CONTRIBUTING.md](CONTRIBUTING.md).
588
665
 
589
666
  ## Troubleshooting
590
667
 
668
+ Run `com2tty --doctor` first: it probes the whole environment (WSL, the
669
+ selected distribution's `python3`, drive automounting, `fuser`, the RFC 2217
670
+ and UF2 relay TCP ports, leftovers from crashed sessions, `/dev/uinput`
671
+ access, and the XInput DLL) and prints one actionable line per check.
672
+
591
673
  At startup com2tty verifies the WSL environment and reports a specific error if
592
674
  a prerequisite is missing. The checks and their remedies are:
593
675
 
@@ -101,6 +101,19 @@ The following options apply to both modes.
101
101
  -l, --list List the serial ports Windows can see (device name,
102
102
  VID:PID, USB bus id, serial number, detected board,
103
103
  description) and exit.
104
+ --json With --list: print the port list as a JSON array
105
+ instead of an aligned table, for scripts and IDE
106
+ integrations.
107
+ --doctor Run an environment self-check (WSL, python3, drive
108
+ automounting, fuser, TCP port availability, leftovers
109
+ from crashed sessions, /dev/uinput, the XInput DLL)
110
+ and exit. Exit status 1 when a required check fails.
111
+ --auto-respawn Rebuild the bridge automatically when the WSL helper
112
+ dies, for example after `wsl --shutdown` or a WSL
113
+ servicing update: com2tty waits until WSL answers
114
+ again and re-creates the same endpoint. In serial
115
+ mode this implies --wait. Applies to serial and
116
+ gamepad mode alike.
104
117
  -d, --debug Enable verbose debug logging on standard error.
105
118
  --distro NAME WSL distribution to use (default: the WSL default
106
119
  distribution). Useful when the default distribution
@@ -122,6 +135,10 @@ port [port ...] Windows COM port(s) to bridge, for example COM3, or
122
135
  --rfc2217-port PORT TCP port for the in-WSL RFC 2217 forwarder
123
136
  (default: 4000). The UF2 relay uses PORT + 1; with
124
137
  multiple ports, each additional port uses PORT + 2i.
138
+ --wait If the COM port is not present yet, wait for it to
139
+ appear instead of failing, then bridge it. Useful
140
+ when com2tty is started before the device is
141
+ plugged in.
125
142
  --bytesize {5,6,7,8} Serial byte size (default: 8).
126
143
  --parity {N,E,O,S,M} Parity: none, even, odd, space, or mark (default: N).
127
144
  --stopbits {1,1.5,2} Stop bits (default: 1).
@@ -139,7 +156,12 @@ port [port ...] Windows COM port(s) to bridge, for example COM3, or
139
156
 
140
157
  ```text
141
158
  --gamepad Select gamepad mode. No COM port is required.
142
- --pad-index {0,1,2,3} XInput controller slot to forward (default: 0).
159
+ --pad-index {0,1,2,3} [...]
160
+ XInput controller slot(s) to forward (default: 0).
161
+ Several slots may be given (--pad-index 0 1) to
162
+ forward multiple controllers at once; each gets its
163
+ own WSL helper and endpoint (/tmp/com2pad0,
164
+ /tmp/com2pad1, ...).
143
165
  --pad-name NAME Device name advertised inside WSL
144
166
  (default: "Microsoft X-Box 360 pad").
145
167
  --uinput Create a real /dev/input device through /dev/uinput
@@ -202,7 +224,15 @@ stale Windows handle and waits for the device to come back, first under its
202
224
  original COM name and then by scanning for its USB serial number, because
203
225
  Windows may assign a different COM number after a replug. Once the device
204
226
  reappears the bridge resumes automatically; the WSL endpoint stays in place
205
- the whole time.
227
+ the whole time. The waiting loops are event-driven: com2tty registers for
228
+ Windows device-change notifications (`WM_DEVICECHANGE`), so a replugged
229
+ device resumes the moment Windows enumerates it rather than on the next
230
+ polling tick (plain polling remains as the fallback).
231
+
232
+ The complementary case — WSL itself going away, for example through
233
+ `wsl --shutdown` or a WSL update — is covered by `--auto-respawn`: instead
234
+ of exiting when the WSL helper dies, com2tty waits until WSL answers again
235
+ and rebuilds the bridge with the same endpoint paths.
206
236
 
207
237
  ### Bridging multiple ports
208
238
 
@@ -253,9 +283,11 @@ com2tty @pad
253
283
  ### Automatic baud-rate detection
254
284
 
255
285
  When the baud rate is left at its default value of `auto`, com2tty queries the
256
- rate that Windows has configured for the port and uses it. If detection fails,
257
- the bridge falls back to 9600 baud. To set the rate explicitly, pass a numeric
258
- value to `--baud`.
286
+ rate that Windows has configured for the port and uses it. The rate is read
287
+ directly from the Win32 `GetCommState` API, which works regardless of the
288
+ Windows display language; parsing the `mode.com` output is kept only as a
289
+ fallback. If detection fails, the bridge falls back to 9600 baud. To set the
290
+ rate explicitly, pass a numeric value to `--baud`.
259
291
 
260
292
  ```cmd
261
293
  com2tty COM3 --baud auto
@@ -343,7 +375,13 @@ these ports during an upload. Run com2tty only on hosts you trust, and choose a
343
375
  non-default `--rfc2217-port` if another local service needs the default port. To
344
376
  reclaim a port left open by a previous com2tty session, the helper only
345
377
  terminates processes whose command line identifies them as a com2tty bridge; an
346
- unrelated service occupying the port is never killed.
378
+ unrelated service occupying the port is never killed. A *running* com2tty
379
+ session is never killed either: each session refreshes a heartbeat marker for
380
+ its ports, so a second invocation that reuses the same `--rfc2217-port` reports
381
+ the conflict and leaves the first bridge intact. Two sessions can run
382
+ concurrently by giving the second one a different `--rfc2217-port` and
383
+ `--wsl-tty`; each session removes only its own block from the shell startup
384
+ files when it exits.
347
385
 
348
386
  ### Gamepad mode
349
387
 
@@ -380,6 +418,24 @@ interprets them using the device profile below. This tier is suited to programs
380
418
  that read the stream directly. Standard applications and game engines that
381
419
  enumerate `/dev/input` devices do not read a FIFO and require the uinput tier.
382
420
 
421
+ Force feedback is available in this tier through a second FIFO created at
422
+ `<path>.ff` (by default `/tmp/com2pad0.ff`): the consumer writes 6-byte rumble
423
+ frames into it — the bytes `0xFB 0xFE` followed by the strong (left,
424
+ low-frequency) and weak (right, high-frequency) motor magnitudes as two
425
+ little-endian unsigned 16-bit values — and com2tty forwards them to the
426
+ physical controller's motors, exactly as the uinput tier does for kernel
427
+ `FF_RUMBLE` effects.
428
+
429
+ To forward several controllers at once, pass several slots:
430
+
431
+ ```cmd
432
+ com2tty --gamepad --pad-index 0 1
433
+ ```
434
+
435
+ Each slot gets its own WSL helper and its own endpoint (`/tmp/com2pad0`,
436
+ `/tmp/com2pad1`, ...); in the uinput tier each helper creates its own
437
+ `/dev/input` device, as if several physical controllers were attached.
438
+
383
439
  #### Opt-in tier: a real device through /dev/uinput
384
440
 
385
441
  The opt-in tier creates a real system-wide device under `/dev/input` so that SDL2
@@ -456,8 +512,9 @@ feedback. When a game or emulator inside WSL plays a rumble effect, the
456
512
  effect's magnitudes travel back through the bridge to the Windows host, which
457
513
  drives the physical controller's motors through `XInputSetState`. The strong
458
514
  (left, low-frequency) and weak (right, high-frequency) motors map directly to
459
- their XInput counterparts. The `/tmp` stream tier has no reverse channel and
460
- therefore no force feedback.
515
+ their XInput counterparts. In the `/tmp` stream tier the same reverse channel
516
+ is reached by writing rumble frames into the `<path>.ff` FIFO, as described
517
+ in [the default tier](#default-tier-the-tmp-event-stream).
461
518
 
462
519
  The forwarded signal matches a real controller at the level of these event codes,
463
520
  ranges, and resolutions, but it is not bit-for-bit identical to a controller
@@ -471,48 +528,68 @@ available. These differences are inherent to the approach.
471
528
 
472
529
  The package is organised around a host process on Windows and a helper process
473
530
  inside WSL connected by the standard input and output streams of the helper.
531
+ The code under `src/com2tty/` is split by where it runs: `cli/` is the
532
+ command-line layer, `core/` holds the dependency-free definitions both sides
533
+ share, `windows/` runs on the Windows interpreter, and `wsl/` runs on the
534
+ Linux interpreter inside WSL.
474
535
 
475
- `cli.py` parses the command line (after `profiles.py` expands any `@profile`
476
- tokens) and dispatches to an entry function in `host.py`: `run_bridge` in
536
+ `cli/` parses the command line (after `cli/profiles.py` expands any `@profile`
537
+ tokens) and dispatches to an entry function in `windows/`: `run_bridge` in
477
538
  serial mode, `run_multi_bridge` when several ports are given, and
478
- `run_gamepad_bridge` in gamepad mode. `discovery.py` implements `--list`.
479
- `__main__.py` and the console entry point both call `cli.main`, and
480
- `__init__.py` holds the package version.
481
-
482
- `host.py` is the Windows side. In serial mode `run_bridge` opens the COM port with
483
- `pyserial`, spawns the WSL helper with `wsl python3 -u bridge.py`, and runs three
484
- threads: one relays bytes from the COM port to the helper's standard input, one
485
- relays bytes from the helper's standard output to the COM port, and one reads the
486
- helper's standard error. The standard error stream carries a line-oriented control
487
- protocol whose messages are prefixed with `[CONTROL]`; these messages drive
488
- dynamic serial-setting changes, the RFC 2217 session lifecycle, and the UF2 upload
489
- sequence. `host.py` also contains the hot-plug reconnect logic and the routine
490
- that writes a transferred UF2 image to the correct Windows drive. The board
491
- detection and reset sequences live in `boards.py`, the UF2 drive lookup and
492
- AutoPlay suppression in `uf2.py`, and the console colour handling in
493
- `banner.py`; `host.py` re-exports these names for backwards compatibility.
494
-
495
- `bridge.py` is the WSL side for serial forwarding. It creates a pseudo terminal
496
- with `openpty`, symlinks the requested path to the pseudo-terminal slave, falling
497
- back to `/tmp` if the requested path is not writable, and runs a `select` loop
498
- that relays data between the helper's standard input and output and the
499
- pseudo-terminal master. It also starts the RFC 2217 forwarder thread and the UF2
500
- relay thread, writes the PlatformIO environment variables into `~/.bashrc`, and
501
- installs the `picotool` interceptor. `rfc2217_server.py` provides the redirector
502
- that implements the RFC 2217 protocol for the forwarder.
503
-
504
- The gamepad path reuses the same spawn-and-pipe transport. `xinput.py` is the
505
- Windows side: it polls an XInput controller slot through `ctypes` (preferring
506
- the `XInputGetStateEx` export so the Guide button is visible) and packs each
507
- state snapshot into a fixed 16-byte frame, sending a frame only when the state
508
- changes. `pad_bridge.py` is the WSL side: it parses the frames, translates them
509
- into evdev events, and writes them to one of two sinks. The default sink writes to
510
- a `/tmp` FIFO, and the opt-in sink creates a real device through `/dev/uinput`
511
- using raw `ioctl` calls. Both sinks share the same event-encoding code, so the
512
- byte stream they produce is identical. In the uinput sink the helper also
513
- services the kernel's force-feedback upload handshake and streams played
514
- rumble effects back over its stdout, where the host applies them to the
515
- physical controller with `XInputSetState`.
539
+ `run_gamepad_bridge` in gamepad mode. `windows/discovery.py` implements
540
+ `--list` and `windows/doctor.py` implements `--doctor`. `__main__.py` and the
541
+ console entry point both call `cli.main`, and `__init__.py` holds the package
542
+ version.
543
+
544
+ `core/` defines the contracts both interpreters rely on: `core/protocol.py`
545
+ holds the `[CONTROL]` message catalogue and the dispatcher the host routes
546
+ stderr lines through, `core/frames.py` the binary gamepad frame codecs,
547
+ `core/boards.py` the USB VID classification with the reset timing data, and
548
+ `core/constants.py` the shared paths, ports, and marker strings.
549
+
550
+ `windows/` is the Windows side. In serial mode `run_bridge` in
551
+ `windows/bridge_app.py` opens the COM port with `pyserial`, spawns the WSL
552
+ helper with `wsl python3 -u bridge.py` (through `windows/wsl_process.py`), and
553
+ runs three threads: one relays bytes from the COM port to the helper's
554
+ standard input, one relays bytes from the helper's standard output to the COM
555
+ port, and one reads the helper's standard error. The standard error stream
556
+ carries a line-oriented control protocol whose messages are prefixed with
557
+ `[CONTROL]`; the handlers in `windows/control_handler.py` drive dynamic
558
+ serial-setting changes, the RFC 2217 session lifecycle, and the UF2 upload
559
+ sequence (including the routine that writes a transferred UF2 image to the
560
+ correct Windows drive, via `windows/uf2_flash.py`). The hot-plug reconnect
561
+ logic lives in `windows/serial_host.py` and the board reset sequences in
562
+ `windows/board_reset.py`. The raw OS-level interventions -- AutoPlay
563
+ suppression, Explorer window closing, `WM_DEVICECHANGE` wake-ups, and console
564
+ VT mode -- are isolated under `windows/os_hacks/`.
565
+
566
+ `wsl/` is the WSL side, restricted to the Python standard library.
567
+ `wsl/serial_app.py` (launched through the `bridge.py` shim at the package
568
+ root) creates a pseudo terminal with `openpty` via `wsl/pty_manager.py`,
569
+ symlinks the requested path to the pseudo-terminal slave, falling back to
570
+ `/tmp` if the requested path is not writable, and runs a `select` loop that
571
+ relays data between the helper's standard input and output and the
572
+ pseudo-terminal master. It also starts the RFC 2217 forwarder and UF2 relay
573
+ threads (`wsl/servers/`), writes the PlatformIO environment variables into
574
+ `~/.bashrc` (`wsl/integrations/shell_env.py`), and installs the `picotool`
575
+ interceptor (`wsl/integrations/picotool.py`).
576
+ `windows/rfc2217_redirector.py` provides the redirector that implements the
577
+ RFC 2217 protocol for the forwarder.
578
+
579
+ The gamepad path reuses the same spawn-and-pipe transport.
580
+ `windows/gamepad_host.py` is the Windows side: it polls an XInput controller
581
+ slot through `ctypes` (preferring the `XInputGetStateEx` export so the Guide
582
+ button is visible) and packs each state snapshot into a fixed 16-byte frame,
583
+ sending a frame only when the state changes. `wsl/gamepad_app.py` (launched
584
+ through the `pad_bridge.py` shim) is the WSL side: it parses the frames,
585
+ translates them into evdev events, and writes them to one of the two sinks in
586
+ `wsl/evdev_sink.py`. The default sink writes to a `/tmp` FIFO, and the opt-in
587
+ sink creates a real device through `/dev/uinput` using raw `ioctl` calls. Both
588
+ sinks share the same event-encoding code, so the byte stream they produce is
589
+ identical. In the uinput sink the helper also services the kernel's
590
+ force-feedback upload handshake and streams played rumble effects back over
591
+ its stdout, where the host applies them to the physical controller with
592
+ `XInputSetState`.
516
593
 
517
594
  For a detailed account of the control protocol, the board reset sequences, the
518
595
  reconnection model, the binary frame formats, and the known hardware-unverified
@@ -572,6 +649,11 @@ step, is documented in [CONTRIBUTING.md](CONTRIBUTING.md).
572
649
 
573
650
  ## Troubleshooting
574
651
 
652
+ Run `com2tty --doctor` first: it probes the whole environment (WSL, the
653
+ selected distribution's `python3`, drive automounting, `fuser`, the RFC 2217
654
+ and UF2 relay TCP ports, leftovers from crashed sessions, `/dev/uinput`
655
+ access, and the XInput DLL) and prints one actionable line per check.
656
+
575
657
  At startup com2tty verifies the WSL environment and reports a specific error if
576
658
  a prerequisite is missing. The checks and their remedies are:
577
659
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "com2tty"
7
- version = "0.2.0"
7
+ version = "0.3.0"
8
8
  description = "A Windows COM port to WSL ttyUSB forwarder"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -33,6 +33,9 @@ package-dir = {"" = "src"}
33
33
  [tool.setuptools.packages.find]
34
34
  where = ["src"]
35
35
 
36
+ [tool.setuptools.package-data]
37
+ "com2tty.wsl" = ["assets/*.in"]
38
+
36
39
  [tool.ruff]
37
40
  target-version = "py38"
38
41
  line-length = 120
@@ -0,0 +1 @@
1
+ __version__ = "0.3.0"
@@ -0,0 +1,21 @@
1
+ """Entry shim for the WSL serial-bridge helper.
2
+
3
+ The Windows host launches this file directly inside WSL with
4
+ ``wsl --exec python3 -u .../com2tty/bridge.py``, so it cannot rely on the
5
+ package being importable: when run as a standalone script it puts the
6
+ package's parent directory on ``sys.path`` first. Keeping this file at the
7
+ package root (rather than moving it into ``com2tty/wsl``) preserves the
8
+ exact path the host resolves and verifies (see ``--doctor``).
9
+
10
+ The implementation lives in ``com2tty.wsl`` (``serial_app`` and friends).
11
+ """
12
+ import os
13
+ import sys
14
+
15
+ if __package__ in (None, ""): # executed as a script inside WSL
16
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
17
+
18
+ from com2tty.wsl.serial_app import main # noqa: E402
19
+
20
+ if __name__ == "__main__": # pragma: no cover
21
+ main()