com2tty 0.2.0__tar.gz → 0.3.1__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 (92) hide show
  1. {com2tty-0.2.0/src/com2tty.egg-info → com2tty-0.3.1}/PKG-INFO +160 -58
  2. {com2tty-0.2.0 → com2tty-0.3.1}/README.md +159 -57
  3. {com2tty-0.2.0 → com2tty-0.3.1}/pyproject.toml +4 -1
  4. com2tty-0.3.1/src/com2tty/__init__.py +1 -0
  5. com2tty-0.3.1/src/com2tty/bridge.py +21 -0
  6. com2tty-0.2.0/src/com2tty/cli.py → com2tty-0.3.1/src/com2tty/cli/__init__.py +113 -22
  7. {com2tty-0.2.0/src/com2tty → com2tty-0.3.1/src/com2tty/cli}/profiles.py +20 -3
  8. com2tty-0.3.1/src/com2tty/core/__init__.py +6 -0
  9. com2tty-0.3.1/src/com2tty/core/boards.py +77 -0
  10. com2tty-0.3.1/src/com2tty/core/constants.py +91 -0
  11. com2tty-0.3.1/src/com2tty/core/frames.py +143 -0
  12. com2tty-0.3.1/src/com2tty/core/protocol.py +129 -0
  13. com2tty-0.3.1/src/com2tty/pad_bridge.py +21 -0
  14. com2tty-0.3.1/src/com2tty/windows/__init__.py +6 -0
  15. com2tty-0.2.0/src/com2tty/boards.py → com2tty-0.3.1/src/com2tty/windows/board_reset.py +27 -80
  16. com2tty-0.3.1/src/com2tty/windows/bridge_app.py +404 -0
  17. com2tty-0.3.1/src/com2tty/windows/control_handler.py +482 -0
  18. {com2tty-0.2.0/src/com2tty → com2tty-0.3.1/src/com2tty/windows}/discovery.py +13 -4
  19. com2tty-0.3.1/src/com2tty/windows/doctor.py +210 -0
  20. com2tty-0.3.1/src/com2tty/windows/gamepad_app.py +226 -0
  21. com2tty-0.2.0/src/com2tty/xinput.py → com2tty-0.3.1/src/com2tty/windows/gamepad_host.py +24 -62
  22. com2tty-0.3.1/src/com2tty/windows/os_hacks/__init__.py +12 -0
  23. com2tty-0.2.0/src/com2tty/uf2.py → com2tty-0.3.1/src/com2tty/windows/os_hacks/autoplay.py +16 -81
  24. com2tty-0.2.0/src/com2tty/banner.py → com2tty-0.3.1/src/com2tty/windows/os_hacks/console.py +10 -0
  25. com2tty-0.3.1/src/com2tty/windows/os_hacks/device_watcher.py +205 -0
  26. com2tty-0.3.1/src/com2tty/windows/os_hacks/explorer.py +154 -0
  27. com2tty-0.2.0/src/com2tty/rfc2217_server.py → com2tty-0.3.1/src/com2tty/windows/rfc2217_redirector.py +29 -0
  28. com2tty-0.3.1/src/com2tty/windows/serial_host.py +313 -0
  29. com2tty-0.3.1/src/com2tty/windows/uf2_flash.py +84 -0
  30. com2tty-0.3.1/src/com2tty/windows/wsl_process.py +264 -0
  31. com2tty-0.3.1/src/com2tty/wsl/__init__.py +14 -0
  32. com2tty-0.3.1/src/com2tty/wsl/assets/picotool_wrapper.py.in +48 -0
  33. com2tty-0.2.0/src/com2tty/pad_bridge.py → com2tty-0.3.1/src/com2tty/wsl/evdev_sink.py +109 -205
  34. com2tty-0.3.1/src/com2tty/wsl/gamepad_app.py +99 -0
  35. com2tty-0.3.1/src/com2tty/wsl/integrations/__init__.py +9 -0
  36. com2tty-0.3.1/src/com2tty/wsl/integrations/picotool.py +133 -0
  37. com2tty-0.3.1/src/com2tty/wsl/integrations/shell_env.py +225 -0
  38. com2tty-0.3.1/src/com2tty/wsl/liveness.py +95 -0
  39. com2tty-0.3.1/src/com2tty/wsl/pty_manager.py +129 -0
  40. com2tty-0.3.1/src/com2tty/wsl/serial_app.py +284 -0
  41. com2tty-0.3.1/src/com2tty/wsl/servers/__init__.py +8 -0
  42. com2tty-0.3.1/src/com2tty/wsl/servers/base.py +144 -0
  43. com2tty-0.3.1/src/com2tty/wsl/servers/rfc2217_forwarder.py +85 -0
  44. com2tty-0.3.1/src/com2tty/wsl/servers/uf2_relay.py +117 -0
  45. {com2tty-0.2.0 → com2tty-0.3.1/src/com2tty.egg-info}/PKG-INFO +160 -58
  46. com2tty-0.3.1/src/com2tty.egg-info/SOURCES.txt +84 -0
  47. com2tty-0.3.1/tests/test_autoplay.py +267 -0
  48. com2tty-0.3.1/tests/test_board_reset.py +71 -0
  49. {com2tty-0.2.0 → com2tty-0.3.1}/tests/test_boards.py +19 -17
  50. com2tty-0.3.1/tests/test_bridge_app.py +821 -0
  51. {com2tty-0.2.0 → com2tty-0.3.1}/tests/test_cli.py +104 -8
  52. com2tty-0.3.1/tests/test_console.py +79 -0
  53. com2tty-0.3.1/tests/test_control_handler.py +1151 -0
  54. com2tty-0.3.1/tests/test_core_frames.py +109 -0
  55. com2tty-0.3.1/tests/test_core_protocol.py +140 -0
  56. com2tty-0.3.1/tests/test_devnotify.py +237 -0
  57. {com2tty-0.2.0 → com2tty-0.3.1}/tests/test_discovery.py +25 -2
  58. com2tty-0.3.1/tests/test_doctor.py +323 -0
  59. com2tty-0.3.1/tests/test_entry_shims.py +80 -0
  60. com2tty-0.2.0/tests/test_pad_bridge.py → com2tty-0.3.1/tests/test_evdev_sink.py +201 -91
  61. com2tty-0.3.1/tests/test_explorer.py +204 -0
  62. com2tty-0.3.1/tests/test_gamepad_app.py +356 -0
  63. com2tty-0.3.1/tests/test_liveness.py +158 -0
  64. com2tty-0.3.1/tests/test_picotool.py +333 -0
  65. {com2tty-0.2.0 → com2tty-0.3.1}/tests/test_profiles.py +36 -5
  66. com2tty-0.3.1/tests/test_pty_manager.py +119 -0
  67. com2tty-0.3.1/tests/test_rfc2217_forwarder.py +173 -0
  68. com2tty-0.2.0/tests/test_rfc2217_server.py → com2tty-0.3.1/tests/test_rfc2217_redirector.py +64 -3
  69. com2tty-0.3.1/tests/test_serial_app.py +535 -0
  70. com2tty-0.3.1/tests/test_serial_host.py +493 -0
  71. com2tty-0.3.1/tests/test_shell_env.py +489 -0
  72. com2tty-0.3.1/tests/test_uf2_flash.py +108 -0
  73. com2tty-0.3.1/tests/test_uf2_relay.py +366 -0
  74. com2tty-0.3.1/tests/test_wsl_gamepad_app.py +51 -0
  75. com2tty-0.3.1/tests/test_wsl_process.py +275 -0
  76. com2tty-0.3.1/tests/test_wsl_servers_base.py +103 -0
  77. {com2tty-0.2.0 → com2tty-0.3.1}/tests/test_xinput.py +33 -29
  78. com2tty-0.2.0/src/com2tty/__init__.py +0 -1
  79. com2tty-0.2.0/src/com2tty/bridge.py +0 -666
  80. com2tty-0.2.0/src/com2tty/host.py +0 -1166
  81. com2tty-0.2.0/src/com2tty.egg-info/SOURCES.txt +0 -33
  82. com2tty-0.2.0/tests/test_bridge_script.py +0 -1572
  83. com2tty-0.2.0/tests/test_host.py +0 -3021
  84. {com2tty-0.2.0 → com2tty-0.3.1}/LICENSE +0 -0
  85. {com2tty-0.2.0 → com2tty-0.3.1}/setup.cfg +0 -0
  86. {com2tty-0.2.0 → com2tty-0.3.1}/setup.py +0 -0
  87. {com2tty-0.2.0 → com2tty-0.3.1}/src/com2tty/__main__.py +0 -0
  88. {com2tty-0.2.0 → com2tty-0.3.1}/src/com2tty.egg-info/dependency_links.txt +0 -0
  89. {com2tty-0.2.0 → com2tty-0.3.1}/src/com2tty.egg-info/entry_points.txt +0 -0
  90. {com2tty-0.2.0 → com2tty-0.3.1}/src/com2tty.egg-info/requires.txt +0 -0
  91. {com2tty-0.2.0 → com2tty-0.3.1}/src/com2tty.egg-info/top_level.txt +0 -0
  92. {com2tty-0.2.0 → com2tty-0.3.1}/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.1
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
@@ -116,7 +116,21 @@ The following options apply to both modes.
116
116
  --version Print the com2tty version and exit.
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
- description) and exit.
119
+ description) and exit. Supplying a COM port together
120
+ with --list is an argument error.
121
+ --json With --list: print the port list as a JSON array
122
+ instead of an aligned table, for scripts and IDE
123
+ integrations.
124
+ --doctor Run an environment self-check (WSL, python3, drive
125
+ automounting, fuser, TCP port availability, leftovers
126
+ from crashed sessions, /dev/uinput, the XInput DLL)
127
+ and exit. Exit status 1 when a required check fails.
128
+ --auto-respawn Rebuild the bridge automatically when the WSL helper
129
+ dies, for example after `wsl --shutdown` or a WSL
130
+ servicing update: com2tty waits until WSL answers
131
+ again and re-creates the same endpoint. In serial
132
+ mode this implies --wait. Applies to serial and
133
+ gamepad mode alike.
120
134
  -d, --debug Enable verbose debug logging on standard error.
121
135
  --distro NAME WSL distribution to use (default: the WSL default
122
136
  distribution). Useful when the default distribution
@@ -138,6 +152,10 @@ port [port ...] Windows COM port(s) to bridge, for example COM3, or
138
152
  --rfc2217-port PORT TCP port for the in-WSL RFC 2217 forwarder
139
153
  (default: 4000). The UF2 relay uses PORT + 1; with
140
154
  multiple ports, each additional port uses PORT + 2i.
155
+ --wait If the COM port is not present yet, wait for it to
156
+ appear instead of failing, then bridge it. Useful
157
+ when com2tty is started before the device is
158
+ plugged in.
141
159
  --bytesize {5,6,7,8} Serial byte size (default: 8).
142
160
  --parity {N,E,O,S,M} Parity: none, even, odd, space, or mark (default: N).
143
161
  --stopbits {1,1.5,2} Stop bits (default: 1).
@@ -155,7 +173,12 @@ port [port ...] Windows COM port(s) to bridge, for example COM3, or
155
173
 
156
174
  ```text
157
175
  --gamepad Select gamepad mode. No COM port is required.
158
- --pad-index {0,1,2,3} XInput controller slot to forward (default: 0).
176
+ --pad-index {0,1,2,3} [...]
177
+ XInput controller slot(s) to forward (default: 0).
178
+ Several slots may be given (--pad-index 0 1) to
179
+ forward multiple controllers at once; each gets its
180
+ own WSL helper and endpoint (/tmp/com2pad0,
181
+ /tmp/com2pad1, ...).
159
182
  --pad-name NAME Device name advertised inside WSL
160
183
  (default: "Microsoft X-Box 360 pad").
161
184
  --uinput Create a real /dev/input device through /dev/uinput
@@ -218,7 +241,15 @@ stale Windows handle and waits for the device to come back, first under its
218
241
  original COM name and then by scanning for its USB serial number, because
219
242
  Windows may assign a different COM number after a replug. Once the device
220
243
  reappears the bridge resumes automatically; the WSL endpoint stays in place
221
- the whole time.
244
+ the whole time. The waiting loops are event-driven: com2tty registers for
245
+ Windows device-change notifications (`WM_DEVICECHANGE`), so a replugged
246
+ device resumes the moment Windows enumerates it rather than on the next
247
+ polling tick (plain polling remains as the fallback).
248
+
249
+ The complementary case — WSL itself going away, for example through
250
+ `wsl --shutdown` or a WSL update — is covered by `--auto-respawn`: instead
251
+ of exiting when the WSL helper dies, com2tty waits until WSL answers again
252
+ and rebuilds the bridge with the same endpoint paths.
222
253
 
223
254
  ### Bridging multiple ports
224
255
 
@@ -266,12 +297,18 @@ com2tty @myboard --baud 9600
266
297
  com2tty @pad
267
298
  ```
268
299
 
300
+ A literal argument that must begin with `@` is written with a doubled marker:
301
+ `@@value` is passed through as the literal `@value` and is never interpreted as
302
+ a profile reference.
303
+
269
304
  ### Automatic baud-rate detection
270
305
 
271
306
  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`.
307
+ rate that Windows has configured for the port and uses it. The rate is read
308
+ directly from the Win32 `GetCommState` API, which works regardless of the
309
+ Windows display language; parsing the `mode.com` output is kept only as a
310
+ fallback. If detection fails, the bridge falls back to 9600 baud. To set the
311
+ rate explicitly, pass a numeric value to `--baud`.
275
312
 
276
313
  ```cmd
277
314
  com2tty COM3 --baud auto
@@ -316,9 +353,11 @@ when zsh is in use or that file exists, and to
316
353
  `~/.config/fish/conf.d/com2tty.fish` when fish is in use or its configuration
317
354
  directory exists. Open a new WSL shell or run `source ~/.bashrc` (or
318
355
  `source ~/.zshrc`; fish picks the snippet up automatically) after starting
319
- com2tty for them to take effect. The variables are removed when com2tty exits;
320
- if a session is killed before it can clean up, the next run removes the stale
321
- block on startup.
356
+ com2tty for them to take effect. The variables are removed when com2tty exits,
357
+ including when the console window is closed: the WSL helper is reaped together
358
+ with the host process rather than left running, and it is given the chance to
359
+ run its own cleanup before it is forced down. If a session is nonetheless killed
360
+ before it can clean up, the next run removes the stale block on startup.
322
361
 
323
362
  Second, com2tty detects the connected board type from its USB vendor identifier
324
363
  and performs the appropriate hardware reset on the Windows side. For ESP32-class
@@ -341,10 +380,13 @@ inside WSL. When PlatformIO calls `picotool` to flash a `.uf2` image, a wrapper
341
380
  transfers the image back to the Windows host over a relay that listens on
342
381
  `127.0.0.1:<rfc2217-port + 1>`. The host then triggers BOOTSEL mode, locates the
343
382
  board's mass-storage drive, verifies the transferred image against an MD5
344
- checksum, and writes the image to the drive. The original `picotool` is restored
345
- when com2tty exits; if a session is killed before it can restore it, the next run
346
- detects and reverses the leftover interception on startup, so PlatformIO uploads
347
- are not left broken.
383
+ checksum, and writes the image to the drive. Subcommands that carry no firmware
384
+ image, such as `picotool info`, `picotool reboot`, and `picotool help`, are
385
+ forwarded to the real binary unchanged, so non-flashing uses of `picotool`
386
+ continue to work while the interception is active. The original `picotool` is
387
+ restored when com2tty exits; if a session is killed before it can restore it, the
388
+ next run detects and reverses the leftover interception on startup, so PlatformIO
389
+ uploads are not left broken.
348
390
 
349
391
  These mechanisms operate without any additional flags. The startup banner reports
350
392
  the detected board type, the RFC 2217 port, the UF2 relay port, and the board's
@@ -359,7 +401,15 @@ these ports during an upload. Run com2tty only on hosts you trust, and choose a
359
401
  non-default `--rfc2217-port` if another local service needs the default port. To
360
402
  reclaim a port left open by a previous com2tty session, the helper only
361
403
  terminates processes whose command line identifies them as a com2tty bridge; an
362
- unrelated service occupying the port is never killed.
404
+ unrelated service occupying the port is never killed. A *running* com2tty
405
+ session is never disturbed: before it touches any shared state, a new session
406
+ checks whether its RFC 2217 and UF2 relay ports are already bound and, if so,
407
+ refuses to start rather than overwrite the first session's shell configuration
408
+ or steal its serial endpoint. As a further guard, the WSL helper refuses to
409
+ replace a tty symlink that already points at another live session's pseudo
410
+ terminal. Two sessions can run concurrently by giving the second one a different
411
+ `--rfc2217-port` and `--wsl-tty`; each session removes only its own block from
412
+ the shell startup files when it exits.
363
413
 
364
414
  ### Gamepad mode
365
415
 
@@ -385,7 +435,10 @@ privileges at run time.
385
435
  #### Default tier: the /tmp event stream
386
436
 
387
437
  The default tier writes the evdev event stream to a FIFO under `/tmp`, by default
388
- `/tmp/com2pad0`, and requires no privileged setup.
438
+ `/tmp/com2pad0`, and requires no privileged setup. This FIFO and its
439
+ force-feedback companion (described below) are created with owner-only
440
+ permissions, so another local user on the WSL instance cannot read the input
441
+ stream or inject events into it.
389
442
 
390
443
  ```cmd
391
444
  com2tty --gamepad
@@ -396,6 +449,24 @@ interprets them using the device profile below. This tier is suited to programs
396
449
  that read the stream directly. Standard applications and game engines that
397
450
  enumerate `/dev/input` devices do not read a FIFO and require the uinput tier.
398
451
 
452
+ Force feedback is available in this tier through a second FIFO created at
453
+ `<path>.ff` (by default `/tmp/com2pad0.ff`): the consumer writes 6-byte rumble
454
+ frames into it — the bytes `0xFB 0xFE` followed by the strong (left,
455
+ low-frequency) and weak (right, high-frequency) motor magnitudes as two
456
+ little-endian unsigned 16-bit values — and com2tty forwards them to the
457
+ physical controller's motors, exactly as the uinput tier does for kernel
458
+ `FF_RUMBLE` effects.
459
+
460
+ To forward several controllers at once, pass several slots:
461
+
462
+ ```cmd
463
+ com2tty --gamepad --pad-index 0 1
464
+ ```
465
+
466
+ Each slot gets its own WSL helper and its own endpoint (`/tmp/com2pad0`,
467
+ `/tmp/com2pad1`, ...); in the uinput tier each helper creates its own
468
+ `/dev/input` device, as if several physical controllers were attached.
469
+
399
470
  #### Opt-in tier: a real device through /dev/uinput
400
471
 
401
472
  The opt-in tier creates a real system-wide device under `/dev/input` so that SDL2
@@ -472,8 +543,9 @@ feedback. When a game or emulator inside WSL plays a rumble effect, the
472
543
  effect's magnitudes travel back through the bridge to the Windows host, which
473
544
  drives the physical controller's motors through `XInputSetState`. The strong
474
545
  (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.
546
+ their XInput counterparts. In the `/tmp` stream tier the same reverse channel
547
+ is reached by writing rumble frames into the `<path>.ff` FIFO, as described
548
+ in [the default tier](#default-tier-the-tmp-event-stream).
477
549
 
478
550
  The forwarded signal matches a real controller at the level of these event codes,
479
551
  ranges, and resolutions, but it is not bit-for-bit identical to a controller
@@ -487,48 +559,68 @@ available. These differences are inherent to the approach.
487
559
 
488
560
  The package is organised around a host process on Windows and a helper process
489
561
  inside WSL connected by the standard input and output streams of the helper.
562
+ The code under `src/com2tty/` is split by where it runs: `cli/` is the
563
+ command-line layer, `core/` holds the dependency-free definitions both sides
564
+ share, `windows/` runs on the Windows interpreter, and `wsl/` runs on the
565
+ Linux interpreter inside WSL.
490
566
 
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
567
+ `cli/` parses the command line (after `cli/profiles.py` expands any `@profile`
568
+ tokens) and dispatches to an entry function in `windows/`: `run_bridge` in
493
569
  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`.
570
+ `run_gamepad_bridge` in gamepad mode. `windows/discovery.py` implements
571
+ `--list` and `windows/doctor.py` implements `--doctor`. `__main__.py` and the
572
+ console entry point both call `cli.main`, and `__init__.py` holds the package
573
+ version.
574
+
575
+ `core/` defines the contracts both interpreters rely on: `core/protocol.py`
576
+ holds the `[CONTROL]` message catalogue and the dispatcher the host routes
577
+ stderr lines through, `core/frames.py` the binary gamepad frame codecs,
578
+ `core/boards.py` the USB VID classification with the reset timing data, and
579
+ `core/constants.py` the shared paths, ports, and marker strings.
580
+
581
+ `windows/` is the Windows side. In serial mode `run_bridge` in
582
+ `windows/bridge_app.py` opens the COM port with `pyserial`, spawns the WSL
583
+ helper with `wsl python3 -u bridge.py` (through `windows/wsl_process.py`), and
584
+ runs three threads: one relays bytes from the COM port to the helper's
585
+ standard input, one relays bytes from the helper's standard output to the COM
586
+ port, and one reads the helper's standard error. The standard error stream
587
+ carries a line-oriented control protocol whose messages are prefixed with
588
+ `[CONTROL]`; the handlers in `windows/control_handler.py` drive dynamic
589
+ serial-setting changes, the RFC 2217 session lifecycle, and the UF2 upload
590
+ sequence (including the routine that writes a transferred UF2 image to the
591
+ correct Windows drive, via `windows/uf2_flash.py`). The hot-plug reconnect
592
+ logic lives in `windows/serial_host.py` and the board reset sequences in
593
+ `windows/board_reset.py`. The raw OS-level interventions -- AutoPlay
594
+ suppression, Explorer window closing, `WM_DEVICECHANGE` wake-ups, and console
595
+ VT mode -- are isolated under `windows/os_hacks/`.
596
+
597
+ `wsl/` is the WSL side, restricted to the Python standard library.
598
+ `wsl/serial_app.py` (launched through the `bridge.py` shim at the package
599
+ root) creates a pseudo terminal with `openpty` via `wsl/pty_manager.py`,
600
+ symlinks the requested path to the pseudo-terminal slave, falling back to
601
+ `/tmp` if the requested path is not writable, and runs a `select` loop that
602
+ relays data between the helper's standard input and output and the
603
+ pseudo-terminal master. It also starts the RFC 2217 forwarder and UF2 relay
604
+ threads (`wsl/servers/`), writes the PlatformIO environment variables into
605
+ `~/.bashrc` (`wsl/integrations/shell_env.py`), and installs the `picotool`
606
+ interceptor (`wsl/integrations/picotool.py`).
607
+ `windows/rfc2217_redirector.py` provides the redirector that implements the
608
+ RFC 2217 protocol for the forwarder.
609
+
610
+ The gamepad path reuses the same spawn-and-pipe transport.
611
+ `windows/gamepad_host.py` is the Windows side: it polls an XInput controller
612
+ slot through `ctypes` (preferring the `XInputGetStateEx` export so the Guide
613
+ button is visible) and packs each state snapshot into a fixed 16-byte frame,
614
+ sending a frame only when the state changes. `wsl/gamepad_app.py` (launched
615
+ through the `pad_bridge.py` shim) is the WSL side: it parses the frames,
616
+ translates them into evdev events, and writes them to one of the two sinks in
617
+ `wsl/evdev_sink.py`. The default sink writes to a `/tmp` FIFO, and the opt-in
618
+ sink creates a real device through `/dev/uinput` using raw `ioctl` calls. Both
619
+ sinks share the same event-encoding code, so the byte stream they produce is
620
+ identical. In the uinput sink the helper also services the kernel's
621
+ force-feedback upload handshake and streams played rumble effects back over
622
+ its stdout, where the host applies them to the physical controller with
623
+ `XInputSetState`.
532
624
 
533
625
  For a detailed account of the control protocol, the board reset sequences, the
534
626
  reconnection model, the binary frame formats, and the known hardware-unverified
@@ -588,6 +680,11 @@ step, is documented in [CONTRIBUTING.md](CONTRIBUTING.md).
588
680
 
589
681
  ## Troubleshooting
590
682
 
683
+ Run `com2tty --doctor` first: it probes the whole environment (WSL, the
684
+ selected distribution's `python3`, drive automounting, `fuser`, the RFC 2217
685
+ and UF2 relay TCP ports, leftovers from crashed sessions, `/dev/uinput`
686
+ access, and the XInput DLL) and prints one actionable line per check.
687
+
591
688
  At startup com2tty verifies the WSL environment and reports a specific error if
592
689
  a prerequisite is missing. The checks and their remedies are:
593
690
 
@@ -609,6 +706,11 @@ package; on minimal distributions install it with `sudo apt install psmisc`, or
609
706
  select a different port with `--rfc2217-port` (the UF2 relay always uses that
610
707
  port plus one).
611
708
 
709
+ If com2tty instead refuses to start with a message that a port is already in
710
+ use, a second com2tty session is already bound to that port. Give the new session
711
+ a different `--rfc2217-port`, and a different `--wsl-tty`, to run the two
712
+ concurrently.
713
+
612
714
  The serial-mode environment variables are written to `~/.bashrc`, to `~/.zshrc`
613
715
  when zsh is detected or a `~/.zshrc` file exists, and to
614
716
  `~/.config/fish/conf.d/com2tty.fish` when fish is detected. Users of other