com2tty 0.1.3__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 (97) hide show
  1. com2tty-0.3.0/PKG-INFO +735 -0
  2. com2tty-0.3.0/README.md +719 -0
  3. {com2tty-0.1.3 → com2tty-0.3.0}/pyproject.toml +17 -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.3.0/src/com2tty/cli/__init__.py +338 -0
  7. com2tty-0.3.0/src/com2tty/cli/profiles.py +103 -0
  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.3.0/src/com2tty/windows/board_reset.py +119 -0
  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.3.0/src/com2tty/windows/discovery.py +81 -0
  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.1.3/src/com2tty/xinput.py → com2tty-0.3.0/src/com2tty/windows/gamepad_host.py +43 -23
  22. com2tty-0.3.0/src/com2tty/windows/os_hacks/__init__.py +12 -0
  23. com2tty-0.3.0/src/com2tty/windows/os_hacks/autoplay.py +120 -0
  24. com2tty-0.3.0/src/com2tty/windows/os_hacks/console.py +38 -0
  25. com2tty-0.3.0/src/com2tty/windows/os_hacks/device_watcher.py +197 -0
  26. com2tty-0.3.0/src/com2tty/windows/os_hacks/explorer.py +125 -0
  27. com2tty-0.1.3/src/com2tty/rfc2217_server.py → com2tty-0.3.0/src/com2tty/windows/rfc2217_redirector.py +29 -1
  28. com2tty-0.3.0/src/com2tty/windows/serial_host.py +304 -0
  29. com2tty-0.3.0/src/com2tty/windows/uf2_flash.py +84 -0
  30. com2tty-0.3.0/src/com2tty/windows/wsl_process.py +131 -0
  31. com2tty-0.3.0/src/com2tty/wsl/__init__.py +14 -0
  32. com2tty-0.3.0/src/com2tty/wsl/assets/picotool_wrapper.py.in +42 -0
  33. com2tty-0.1.3/src/com2tty/pad_bridge.py → com2tty-0.3.0/src/com2tty/wsl/evdev_sink.py +218 -177
  34. com2tty-0.3.0/src/com2tty/wsl/gamepad_app.py +99 -0
  35. com2tty-0.3.0/src/com2tty/wsl/integrations/__init__.py +9 -0
  36. com2tty-0.3.0/src/com2tty/wsl/integrations/picotool.py +133 -0
  37. com2tty-0.3.0/src/com2tty/wsl/integrations/shell_env.py +193 -0
  38. com2tty-0.3.0/src/com2tty/wsl/liveness.py +67 -0
  39. com2tty-0.3.0/src/com2tty/wsl/pty_manager.py +100 -0
  40. com2tty-0.3.0/src/com2tty/wsl/serial_app.py +208 -0
  41. com2tty-0.3.0/src/com2tty/wsl/servers/__init__.py +8 -0
  42. com2tty-0.3.0/src/com2tty/wsl/servers/base.py +144 -0
  43. com2tty-0.3.0/src/com2tty/wsl/servers/rfc2217_forwarder.py +85 -0
  44. com2tty-0.3.0/src/com2tty/wsl/servers/uf2_relay.py +117 -0
  45. com2tty-0.3.0/src/com2tty.egg-info/PKG-INFO +735 -0
  46. com2tty-0.3.0/src/com2tty.egg-info/SOURCES.txt +83 -0
  47. com2tty-0.3.0/tests/test_autoplay.py +267 -0
  48. com2tty-0.3.0/tests/test_board_reset.py +71 -0
  49. com2tty-0.3.0/tests/test_boards.py +143 -0
  50. com2tty-0.3.0/tests/test_bridge_app.py +821 -0
  51. com2tty-0.3.0/tests/test_cli.py +313 -0
  52. com2tty-0.3.0/tests/test_console.py +79 -0
  53. com2tty-0.3.0/tests/test_control_handler.py +1151 -0
  54. com2tty-0.3.0/tests/test_core_frames.py +109 -0
  55. com2tty-0.3.0/tests/test_core_protocol.py +129 -0
  56. com2tty-0.3.0/tests/test_devnotify.py +237 -0
  57. com2tty-0.3.0/tests/test_discovery.py +159 -0
  58. com2tty-0.3.0/tests/test_doctor.py +323 -0
  59. com2tty-0.3.0/tests/test_entry_shims.py +80 -0
  60. com2tty-0.3.0/tests/test_evdev_sink.py +723 -0
  61. com2tty-0.3.0/tests/test_gamepad_app.py +331 -0
  62. com2tty-0.3.0/tests/test_liveness.py +117 -0
  63. com2tty-0.3.0/tests/test_picotool.py +333 -0
  64. com2tty-0.3.0/tests/test_profiles.py +185 -0
  65. com2tty-0.3.0/tests/test_pty_manager.py +73 -0
  66. com2tty-0.3.0/tests/test_rfc2217_forwarder.py +173 -0
  67. com2tty-0.1.3/tests/test_rfc2217_server.py → com2tty-0.3.0/tests/test_rfc2217_redirector.py +93 -3
  68. com2tty-0.3.0/tests/test_serial_app.py +452 -0
  69. com2tty-0.3.0/tests/test_serial_host.py +493 -0
  70. com2tty-0.3.0/tests/test_shell_env.py +426 -0
  71. com2tty-0.3.0/tests/test_uf2_flash.py +108 -0
  72. com2tty-0.3.0/tests/test_uf2_relay.py +366 -0
  73. com2tty-0.3.0/tests/test_wsl_gamepad_app.py +51 -0
  74. com2tty-0.3.0/tests/test_wsl_process.py +110 -0
  75. com2tty-0.3.0/tests/test_wsl_servers_base.py +103 -0
  76. {com2tty-0.1.3 → com2tty-0.3.0}/tests/test_xinput.py +86 -9
  77. com2tty-0.1.3/PKG-INFO +0 -452
  78. com2tty-0.1.3/README.md +0 -436
  79. com2tty-0.1.3/src/com2tty/__init__.py +0 -1
  80. com2tty-0.1.3/src/com2tty/bridge.py +0 -521
  81. com2tty-0.1.3/src/com2tty/cli.py +0 -188
  82. com2tty-0.1.3/src/com2tty/host.py +0 -1019
  83. com2tty-0.1.3/src/com2tty.egg-info/PKG-INFO +0 -452
  84. com2tty-0.1.3/src/com2tty.egg-info/SOURCES.txt +0 -25
  85. com2tty-0.1.3/tests/test_bridge_script.py +0 -1070
  86. com2tty-0.1.3/tests/test_cli.py +0 -155
  87. com2tty-0.1.3/tests/test_host.py +0 -1997
  88. com2tty-0.1.3/tests/test_pad_bridge.py +0 -422
  89. {com2tty-0.1.3 → com2tty-0.3.0}/LICENSE +0 -0
  90. {com2tty-0.1.3 → com2tty-0.3.0}/setup.cfg +0 -0
  91. {com2tty-0.1.3 → com2tty-0.3.0}/setup.py +0 -0
  92. {com2tty-0.1.3 → com2tty-0.3.0}/src/com2tty/__main__.py +0 -0
  93. {com2tty-0.1.3 → com2tty-0.3.0}/src/com2tty.egg-info/dependency_links.txt +0 -0
  94. {com2tty-0.1.3 → com2tty-0.3.0}/src/com2tty.egg-info/entry_points.txt +0 -0
  95. {com2tty-0.1.3 → com2tty-0.3.0}/src/com2tty.egg-info/requires.txt +0 -0
  96. {com2tty-0.1.3 → com2tty-0.3.0}/src/com2tty.egg-info/top_level.txt +0 -0
  97. {com2tty-0.1.3 → com2tty-0.3.0}/tests/test_main.py +0 -0
com2tty-0.3.0/PKG-INFO ADDED
@@ -0,0 +1,735 @@
1
+ Metadata-Version: 2.4
2
+ Name: com2tty
3
+ Version: 0.3.0
4
+ Summary: A Windows COM port to WSL ttyUSB forwarder
5
+ Author-email: yichengs <yichengs.tw+com2tty@gmail.com>
6
+ Project-URL: Homepage, https://github.com/Yi-Cheng-Wang/com2tty
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: Microsoft :: Windows
10
+ Classifier: Operating System :: POSIX :: Linux
11
+ Requires-Python: >=3.8
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: pyserial>=3.5
15
+ Dynamic: license-file
16
+
17
+ # com2tty
18
+
19
+ `com2tty` is a Python package that runs on a Windows host and forwards a device
20
+ attached to Windows into a Windows Subsystem for Linux (WSL) instance, where it
21
+ appears as a native Linux device. It supports two kinds of forwarding. The first
22
+ forwards a Windows COM port into WSL as a virtual serial device such as
23
+ `/tmp/ttyUSB0`. The second forwards a Windows XInput game controller into WSL as
24
+ a Linux evdev gamepad. Both kinds use the same transport: a low-latency,
25
+ firewall-resilient bridge built on standard input and output redirection between
26
+ the Windows host process and a helper process running inside WSL. No network
27
+ configuration, port forwarding, or firewall change is required.
28
+
29
+ The intended users are developers who work inside WSL but whose hardware is bound
30
+ to the Windows host: embedded developers who flash and monitor microcontrollers
31
+ over USB-to-serial adapters, and developers who need a game controller available
32
+ to Linux tools running in WSL.
33
+
34
+ ## Table of contents
35
+
36
+ - [Requirements](#requirements)
37
+ - [Installation](#installation)
38
+ - [Configuration](#configuration)
39
+ - [Usage](#usage)
40
+ - [Listing available ports](#listing-available-ports)
41
+ - [Bridging a serial port](#bridging-a-serial-port)
42
+ - [Hot-plug auto-reconnect](#hot-plug-auto-reconnect)
43
+ - [Bridging multiple ports](#bridging-multiple-ports)
44
+ - [Argument profiles](#argument-profiles)
45
+ - [Automatic baud-rate detection](#automatic-baud-rate-detection)
46
+ - [Configuring /dev/ttyUSB0 in WSL](#configuring-devttyusb0-in-wsl)
47
+ - [Firmware upload through the bridge](#firmware-upload-through-the-bridge)
48
+ - [Gamepad mode](#gamepad-mode)
49
+ - [Architecture overview](#architecture-overview)
50
+ - [Development setup](#development-setup)
51
+ - [Contributing](#contributing)
52
+ - [Troubleshooting](#troubleshooting)
53
+ - [License](#license)
54
+
55
+ ## Requirements
56
+
57
+ The Windows host requires Python 3.8 or later. The `pyserial` package, version
58
+ 3.5 or later, is the only runtime dependency and is installed automatically with
59
+ the package. A working WSL installation is required, and the WSL distribution
60
+ must provide `python3` on its `PATH`. By default the WSL default distribution
61
+ is used; a specific one can be selected with `--distro`. The WSL helper uses
62
+ only the Python standard library and therefore needs no additional packages
63
+ inside WSL. These prerequisites are verified at startup and a specific,
64
+ actionable error is reported when one is missing.
65
+
66
+ Serial forwarding requires a COM port that Windows can open. Gamepad forwarding
67
+ requires a controller that the Windows XInput driver recognises, which is the
68
+ standard case for Xbox and XInput-compatible controllers. The opt-in gamepad tier
69
+ that creates a real Linux input device additionally requires a one-time
70
+ privileged setup inside WSL, described in [Gamepad mode](#gamepad-mode).
71
+
72
+ ## Installation
73
+
74
+ Install the released package from PyPI on the Windows host.
75
+
76
+ ```cmd
77
+ pip install com2tty
78
+ ```
79
+
80
+ Alternatively, install from a checkout of the source by running the following
81
+ in the project root.
82
+
83
+ ```cmd
84
+ pip install .
85
+ ```
86
+
87
+ To work on the package itself, install it in editable mode.
88
+
89
+ ```cmd
90
+ pip install -e .
91
+ ```
92
+
93
+ Installation registers a console entry point named `com2tty`. If the entry point
94
+ is not on your `PATH`, the package can also be invoked as a module with
95
+ `python -m com2tty`.
96
+
97
+ ## Configuration
98
+
99
+ `com2tty` is configured through command-line arguments, optionally saved as
100
+ named profiles in an INI file (see
101
+ [Argument profiles](#argument-profiles)). Note that in serial mode the WSL
102
+ helper writes environment variables into the WSL user's shell configuration;
103
+ this behaviour is described in
104
+ [Firmware upload through the bridge](#firmware-upload-through-the-bridge).
105
+
106
+ The positional arguments are the COM ports. At least one is required in
107
+ serial mode; several may be given to bridge them concurrently. The port is
108
+ omitted in gamepad mode, which is selected with `--gamepad`, and in the
109
+ `--list` and `--version` modes.
110
+
111
+ ### Options common to both modes
112
+
113
+ The following options apply to both modes.
114
+
115
+ ```text
116
+ --version Print the com2tty version and exit.
117
+ -l, --list List the serial ports Windows can see (device name,
118
+ VID:PID, USB bus id, serial number, detected board,
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.
133
+ -d, --debug Enable verbose debug logging on standard error.
134
+ --distro NAME WSL distribution to use (default: the WSL default
135
+ distribution). Useful when the default distribution
136
+ lacks python3, for example docker-desktop.
137
+ ```
138
+
139
+ ### Serial-mode options
140
+
141
+ ```text
142
+ port [port ...] Windows COM port(s) to bridge, for example COM3, or
143
+ COM3 COM5 to bridge two ports at once. Required
144
+ unless --gamepad or --list is given.
145
+ -b, --baud BAUD Baud rate, or the literal value "auto" to detect the
146
+ rate Windows has configured for the port
147
+ (default: auto; falls back to 9600 if detection fails).
148
+ -w, --wsl-tty PATH Target symlink path created inside WSL
149
+ (default: /tmp/ttyUSB0). With multiple ports, each
150
+ additional port increments the trailing number.
151
+ --rfc2217-port PORT TCP port for the in-WSL RFC 2217 forwarder
152
+ (default: 4000). The UF2 relay uses PORT + 1; with
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.
158
+ --bytesize {5,6,7,8} Serial byte size (default: 8).
159
+ --parity {N,E,O,S,M} Parity: none, even, odd, space, or mark (default: N).
160
+ --stopbits {1,1.5,2} Stop bits (default: 1).
161
+ --xonxoff Enable software flow control (XON/XOFF).
162
+ --rtscts Enable hardware flow control (RTS/CTS).
163
+ --dsrdtr Enable hardware flow control (DSR/DTR).
164
+ --board {auto,esp32,pico,nrf52,samd,stm32,none}
165
+ Override USB VID board detection for reset and upload
166
+ handling (default: auto). Use this when a board uses a
167
+ USB-UART chip that com2tty does not recognise; "none"
168
+ disables board-specific reset sequences entirely.
169
+ ```
170
+
171
+ ### Gamepad-mode options
172
+
173
+ ```text
174
+ --gamepad Select gamepad mode. No COM port is required.
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, ...).
181
+ --pad-name NAME Device name advertised inside WSL
182
+ (default: "Microsoft X-Box 360 pad").
183
+ --uinput Create a real /dev/input device through /dev/uinput
184
+ instead of the default /tmp event stream.
185
+ --wsl-pad PATH FIFO path for the default /tmp event stream
186
+ (default: /tmp/com2pad0).
187
+ --poll-hz HZ XInput polling rate in hertz (default: 250). Frames are
188
+ sent only when the controller state changes.
189
+ ```
190
+
191
+ ## Usage
192
+
193
+ Run `com2tty` from any Windows terminal, either PowerShell or Command Prompt. The
194
+ process runs in the foreground and is stopped with Ctrl+C.
195
+
196
+ ### Listing available ports
197
+
198
+ `com2tty --list` (or `-l`) enumerates every serial port Windows can see,
199
+ without requiring usbipd or administrator rights.
200
+
201
+ ```text
202
+ > com2tty --list
203
+ Device VID:PID Bus ID Serial number Board Description
204
+ ------ --------- ------ ---------------- ------- -----------------------
205
+ COM3 - - - unknown Bluetooth serial (COM3)
206
+ COM17 2E8A:F00F 1-6 98C4FFA253A63FB7 pico USB serial device (COM17)
207
+ ```
208
+
209
+ The `Bus ID` column is the USB bus location (for example `1-6` or `2-6`), read
210
+ directly from the device descriptor. The `Board` column shows the family that
211
+ VID-based detection would assign, which is the same detection the bridge uses
212
+ for reset and upload handling.
213
+
214
+ ### Bridging a serial port
215
+
216
+ Bridge `COM3` to the default WSL path `/tmp/ttyUSB0` at 115200 baud.
217
+
218
+ ```cmd
219
+ com2tty COM3 --baud 115200
220
+ ```
221
+
222
+ Bridge `COM5` to a custom WSL device path at 9600 baud.
223
+
224
+ ```cmd
225
+ com2tty COM5 --baud 9600 -w /tmp/my_device
226
+ ```
227
+
228
+ While the bridge is active, a Linux program inside WSL opens the symlinked path
229
+ and reads from and writes to it as if it were a local serial device. Data is
230
+ relayed in both directions between the Windows COM port and the WSL pseudo
231
+ terminal. Dynamic changes that a WSL program makes to the line settings, such as
232
+ the baud rate, are detected and applied to the underlying Windows COM port.
233
+
234
+ ### Hot-plug auto-reconnect
235
+
236
+ When the bridged device is unplugged, resets, or re-enumerates, the bridge
237
+ does not need to be restarted. After a short grace period for transient
238
+ errors (such as a board rebooting into its bootloader), com2tty closes the
239
+ stale Windows handle and waits for the device to come back, first under its
240
+ original COM name and then by scanning for its USB serial number, because
241
+ Windows may assign a different COM number after a replug. Once the device
242
+ reappears the bridge resumes automatically; the WSL endpoint stays in place
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.
252
+
253
+ ### Bridging multiple ports
254
+
255
+ Passing several ports bridges them all from one invocation.
256
+
257
+ ```cmd
258
+ com2tty COM3 COM5
259
+ ```
260
+
261
+ Each port gets its own WSL helper and endpoint: the symlink path is derived
262
+ from `--wsl-tty` by incrementing its trailing number (`/tmp/ttyUSB0`,
263
+ `/tmp/ttyUSB1`, ...), and the RFC 2217 port is the base value plus two per
264
+ additional port (each bridge also reserves its port plus one for the UF2
265
+ relay). Only the first port writes the PlatformIO environment variables and
266
+ intercepts `picotool`, so concurrent bridges do not overwrite each other's
267
+ shell configuration; tools targeting a secondary port can use its RFC 2217
268
+ port directly.
269
+
270
+ ### Argument profiles
271
+
272
+ Frequently used argument sets can be saved as named profiles in an INI file,
273
+ either `com2tty.ini` in the current directory or `.com2tty.ini` in the user
274
+ profile directory. Keys are the long option names (dashes and underscores are
275
+ both accepted), `port` supplies the positional argument, and boolean keys
276
+ take `true` or `false`.
277
+
278
+ ```ini
279
+ [myboard]
280
+ port = COM5
281
+ baud = 115200
282
+ wsl-tty = /tmp/my_device
283
+ board = pico
284
+
285
+ [pad]
286
+ gamepad = true
287
+ uinput = true
288
+ ```
289
+
290
+ A profile is invoked with an `@` prefix, and arguments given after the token
291
+ override the profile's values.
292
+
293
+ ```cmd
294
+ com2tty @myboard
295
+ com2tty @myboard --baud 9600
296
+ com2tty @pad
297
+ ```
298
+
299
+ ### Automatic baud-rate detection
300
+
301
+ When the baud rate is left at its default value of `auto`, com2tty queries the
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`.
307
+
308
+ ```cmd
309
+ com2tty COM3 --baud auto
310
+ ```
311
+
312
+ ### Configuring /dev/ttyUSB0 in WSL
313
+
314
+ In Linux the `/dev` directory is owned by `root`. Running com2tty as an ordinary
315
+ Windows user means the WSL helper cannot create a symlink directly under `/dev`.
316
+ For this reason the default target is `/tmp/ttyUSB0`, which is user-writable, and
317
+ com2tty never requires elevated privileges at run time. If a path under `/dev` is
318
+ requested and permission is denied, the helper automatically falls back to the
319
+ equivalent path under `/tmp` and prints instructions.
320
+
321
+ To expose the device at a stable `/dev` path without granting com2tty privileges,
322
+ create a one-time symlink inside WSL that points from `/dev` to the stable `/tmp`
323
+ path.
324
+
325
+ ```bash
326
+ sudo ln -sf /tmp/ttyUSB0 /dev/ttyUSB0
327
+ ```
328
+
329
+ Each time com2tty starts, it repoints `/tmp/ttyUSB0` at the active pseudo
330
+ terminal, so `/dev/ttyUSB0` continues to resolve correctly. After this one-time
331
+ step, WSL programs such as `minicom`, `screen`, the ESP-IDF tools, or Python
332
+ scripts can use `/dev/ttyUSB0` directly.
333
+
334
+ ### Firmware upload through the bridge
335
+
336
+ In serial mode com2tty additionally supports flashing microcontroller firmware
337
+ from build tools running inside WSL, so that a PlatformIO project in WSL can
338
+ upload to a board attached to Windows. This support is enabled by default and
339
+ involves three mechanisms.
340
+
341
+ First, the WSL helper starts an RFC 2217 forwarder that listens on
342
+ `127.0.0.1:<rfc2217-port>` inside WSL, where the port defaults to 4000. To make
343
+ PlatformIO use it, the helper appends environment variables to the WSL user's
344
+ shell startup file: `PLATFORMIO_UPLOAD_PORT` is set to
345
+ `rfc2217://127.0.0.1:<rfc2217-port>` and `PLATFORMIO_MONITOR_PORT` is set to the
346
+ serial symlink path. The variables are written to `~/.bashrc`, to `~/.zshrc`
347
+ when zsh is in use or that file exists, and to
348
+ `~/.config/fish/conf.d/com2tty.fish` when fish is in use or its configuration
349
+ directory exists. Open a new WSL shell or run `source ~/.bashrc` (or
350
+ `source ~/.zshrc`; fish picks the snippet up automatically) after starting
351
+ com2tty for them to take effect. The variables are removed when com2tty exits;
352
+ if a session is killed before it can clean up, the next run removes the stale
353
+ block on startup.
354
+
355
+ Second, com2tty detects the connected board type from its USB vendor identifier
356
+ and performs the appropriate hardware reset on the Windows side. For ESP32-class
357
+ boards it performs the DTR and RTS auto-reset sequence to enter the download
358
+ mode. For RP2040 and RP2350 boards, and for Adafruit nRF52 boards with the UF2
359
+ bootloader, it performs the 1200-baud touch that triggers the mass-storage
360
+ bootloader mode. For Arduino Leonardo- and SAMD-class boards it performs the
361
+ same 1200-baud touch; because their bootloader re-enumerates as a separate
362
+ serial port (often with a different USB identity than the application port),
363
+ com2tty snapshots the port list before the touch, waits for the new bootloader
364
+ port to appear, opens it, and runs the upload against it over RFC 2217. When the
365
+ upload finishes and the board reboots into the application, com2tty restores and
366
+ reopens the original application port. For STM32-class boards it pulses
367
+ DTR and RTS in the conventional BOOT0/NRST wiring so the board resets around
368
+ an upload; boards flashed through ST-LINK or DFU are unaffected by the pulse.
369
+
370
+ Third, for UF2-bootloader boards (RP2040, RP2350, and nRF52), com2tty
371
+ intercepts the `picotool` invocation
372
+ inside WSL. When PlatformIO calls `picotool` to flash a `.uf2` image, a wrapper
373
+ transfers the image back to the Windows host over a relay that listens on
374
+ `127.0.0.1:<rfc2217-port + 1>`. The host then triggers BOOTSEL mode, locates the
375
+ board's mass-storage drive, verifies the transferred image against an MD5
376
+ checksum, and writes the image to the drive. The original `picotool` is restored
377
+ when com2tty exits; if a session is killed before it can restore it, the next run
378
+ detects and reverses the leftover interception on startup, so PlatformIO uploads
379
+ are not left broken.
380
+
381
+ These mechanisms operate without any additional flags. The startup banner reports
382
+ the detected board type, the RFC 2217 port, the UF2 relay port, and the board's
383
+ USB serial number. If the detected board type is wrong (for example a board whose
384
+ USB-UART chip is not recognised), override it with `--board`.
385
+
386
+ The RFC 2217 forwarder and the UF2 relay listen on the loopback interface
387
+ (`127.0.0.1`) inside the WSL distribution and perform no authentication. On a
388
+ single-user machine this is not exposed to the network, but on a shared or
389
+ multi-user WSL host any local user in the same distribution could connect to
390
+ these ports during an upload. Run com2tty only on hosts you trust, and choose a
391
+ non-default `--rfc2217-port` if another local service needs the default port. To
392
+ reclaim a port left open by a previous com2tty session, the helper only
393
+ terminates processes whose command line identifies them as a com2tty bridge; an
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.
401
+
402
+ ### Gamepad mode
403
+
404
+ Gamepad mode forwards a Windows XInput controller into WSL. It exists because
405
+ forwarding a controller with `usbipd` does not work in a default WSL2 setup: the
406
+ stock WSL2 kernel is built without the `xpad` driver, so an attached controller
407
+ is enumerated but never produces a usable input device. Gamepad mode keeps the
408
+ controller on Windows, where the native XInput driver handles it, reads its state
409
+ on the Windows side, and streams that state through the same bridge used for
410
+ serial forwarding. Inside WSL a helper, which uses only the Python standard
411
+ library, turns the state into a Linux evdev `input_event` stream describing a
412
+ Microsoft X-Box 360 pad, identified by USB vendor 0x045e and product 0x028e.
413
+
414
+ The controller must be visible to Windows XInput. If the controller has been
415
+ bound or attached with `usbipd`, Windows no longer owns it and XInput reports no
416
+ controller; unbind it from `usbipd` so that Windows holds the controller before
417
+ using gamepad mode.
418
+
419
+ Gamepad mode provides two tiers. Both emit the identical evdev byte stream, so a
420
+ single reader works against either, and com2tty itself never requires elevated
421
+ privileges at run time.
422
+
423
+ #### Default tier: the /tmp event stream
424
+
425
+ The default tier writes the evdev event stream to a FIFO under `/tmp`, by default
426
+ `/tmp/com2pad0`, and requires no privileged setup.
427
+
428
+ ```cmd
429
+ com2tty --gamepad
430
+ ```
431
+
432
+ A consumer inside WSL reads 24-byte Linux `input_event` records from the FIFO and
433
+ interprets them using the device profile below. This tier is suited to programs
434
+ that read the stream directly. Standard applications and game engines that
435
+ enumerate `/dev/input` devices do not read a FIFO and require the uinput tier.
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
+
455
+ #### Opt-in tier: a real device through /dev/uinput
456
+
457
+ The opt-in tier creates a real system-wide device under `/dev/input` so that SDL2
458
+ applications, emulators, and tools such as `evtest` recognise a normally attached
459
+ controller.
460
+
461
+ ```cmd
462
+ com2tty --gamepad --uinput
463
+ ```
464
+
465
+ If `/dev/uinput` is not accessible, com2tty prints the one-time setup instructions
466
+ and automatically falls back to the `/tmp` event stream so that forwarding
467
+ continues to work.
468
+
469
+ #### One-time setup for the uinput tier
470
+
471
+ Creating a real input device requires access to `/dev/uinput`, which Linux
472
+ restricts to `root`, and reading the resulting `/dev/input/event*` node requires
473
+ membership of the `input` group. Both are granted once, inside WSL, and com2tty
474
+ still runs without privileges thereafter. Either use `sudo`, or run the commands
475
+ as root from Windows with `wsl -u root`, which requires no password.
476
+
477
+ ```bash
478
+ sudo modprobe uinput
479
+ sudo chmod 0666 /dev/uinput
480
+ sudo usermod -aG input "$USER"
481
+ ```
482
+
483
+ The permission granted by `chmod` does not survive `wsl --shutdown`. To make it
484
+ persist, add a boot command to `/etc/wsl.conf`, which runs as root on every WSL
485
+ start.
486
+
487
+ ```ini
488
+ [boot]
489
+ command = modprobe uinput && chmod 0666 /dev/uinput
490
+ ```
491
+
492
+ After editing `/etc/wsl.conf`, run `wsl --shutdown` once from Windows. This also
493
+ refreshes the group membership granted by `usermod`.
494
+
495
+ The stock WSL2 kernel sets `CONFIG_INPUT_UINPUT` as a module, which works, but
496
+ does not set `CONFIG_INPUT_JOYDEV`. As a result the legacy `/dev/input/js*` node
497
+ is absent. This is not a problem for modern applications and SDL2, which read
498
+ `/dev/input/event*` directly.
499
+
500
+ #### Verifying the uinput tier
501
+
502
+ With com2tty running in `--uinput` mode, confirm the device inside WSL with
503
+ `evtest`.
504
+
505
+ ```bash
506
+ sudo apt install evtest
507
+ evtest
508
+ ```
509
+
510
+ Select the Microsoft X-Box 360 pad device, then move the sticks and press buttons
511
+ on Windows and observe the events appear in WSL.
512
+
513
+ #### Device profile
514
+
515
+ Both tiers emit the same evdev codes. Buttons are reported as `BTN_A`, `BTN_B`,
516
+ `BTN_X`, `BTN_Y`, `BTN_TL`, `BTN_TR`, `BTN_SELECT`, `BTN_START`, `BTN_THUMBL`,
517
+ `BTN_THUMBR`, and `BTN_MODE` for the Guide (Xbox logo) button. The sticks are
518
+ reported as `ABS_X` and `ABS_Y` for the left
519
+ stick and `ABS_RX` and `ABS_RY` for the right stick, each spanning the signed
520
+ 16-bit range. The triggers are reported as `ABS_Z` for the left trigger and
521
+ `ABS_RZ` for the right trigger, each spanning 0 to 255. The directional pad is
522
+ reported as `ABS_HAT0X` and `ABS_HAT0Y` with values of -1, 0, or 1. The stick Y
523
+ axes are inverted to follow the Linux convention in which pushing up produces a
524
+ negative value.
525
+
526
+ In the uinput tier the virtual pad also advertises `FF_RUMBLE` force
527
+ feedback. When a game or emulator inside WSL plays a rumble effect, the
528
+ effect's magnitudes travel back through the bridge to the Windows host, which
529
+ drives the physical controller's motors through `XInputSetState`. The strong
530
+ (left, low-frequency) and weak (right, high-frequency) motors map directly to
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).
534
+
535
+ The forwarded signal matches a real controller at the level of these event codes,
536
+ ranges, and resolutions, but it is not bit-for-bit identical to a controller
537
+ driven by the kernel `xpad` driver. The timing and latency differ because the
538
+ path is polled and piped rather than delivered by a fixed USB interrupt interval.
539
+ The Guide button is read through the undocumented `XInputGetStateEx` call and is
540
+ reported as zero on systems where only the legacy `xinput9_1_0` DLL is
541
+ available. These differences are inherent to the approach.
542
+
543
+ ## Architecture overview
544
+
545
+ The package is organised around a host process on Windows and a helper process
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.
551
+
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
554
+ serial mode, `run_multi_bridge` when several ports are given, and
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`.
609
+
610
+ For a detailed account of the control protocol, the board reset sequences, the
611
+ reconnection model, the binary frame formats, and the known hardware-unverified
612
+ behaviour, see the [architecture document](ARCHITECTURE.md).
613
+
614
+ ## Development setup
615
+
616
+ Install the package in editable mode together with the test tools.
617
+
618
+ ```bash
619
+ pip install -e .
620
+ pip install pytest pytest-cov ruff
621
+ ```
622
+
623
+ Run the test suite with coverage, and the linter.
624
+
625
+ ```bash
626
+ pytest --cov=src/com2tty --cov-report=term-missing tests/
627
+ ruff check src tests scripts
628
+ ```
629
+
630
+ A passing run reports all tests passing and full line coverage for the package.
631
+ The test suite is cross-platform. The `tests/conftest.py` file substitutes a mock
632
+ `termios` module on Windows so that the WSL-side modules import for testing, and
633
+ the platform-specific system calls used by the gamepad sinks are mocked so that
634
+ the suite runs on both Windows and Linux.
635
+
636
+ Continuous integration is defined in `.github/workflows/ci.yml`. It runs a ruff
637
+ lint job and the test suite on `windows-latest` and `ubuntu-latest` against
638
+ Python 3.8 through 3.13, and it enforces 100 percent line coverage by running
639
+ pytest with `--cov-fail-under=100`. A separate job publishes the package to PyPI
640
+ on pushes to the `main` branch.
641
+
642
+ Because CI has no WSL or serial hardware, a manual end-to-end smoke test lives
643
+ at `scripts/e2e_smoke.py`. Run it on a real Windows host with a device attached
644
+ before a release: it checks `--list`, starts a bridge, verifies the symlink and
645
+ the RFC 2217 forwarder inside WSL, and optionally verifies an echo round-trip
646
+ when the device has TX wired to RX (`--loopback`).
647
+
648
+ ```cmd
649
+ python scripts/e2e_smoke.py --port COM17
650
+ ```
651
+
652
+ ## Contributing
653
+
654
+ Base feature branches on the `develop` branch and open pull requests against it.
655
+ Commit messages follow the Conventional Commits format, for example
656
+ `feat(gamepad): ...` or `test(host): ...`, as established in the project history.
657
+ Every change must keep the test suite passing with 100 percent line coverage on
658
+ both Windows and Ubuntu across the supported Python versions, and must pass the
659
+ ruff linter, because continuous integration enforces both. Add or update tests
660
+ for any behavioural change.
661
+
662
+ The full contribution process, including how to report bugs, the commit
663
+ convention, the linting requirements, and the manual end-to-end verification
664
+ step, is documented in [CONTRIBUTING.md](CONTRIBUTING.md).
665
+
666
+ ## Troubleshooting
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
+
673
+ At startup com2tty verifies the WSL environment and reports a specific error if
674
+ a prerequisite is missing. The checks and their remedies are:
675
+
676
+ - `wsl.exe` is not on `PATH`: install WSL with `wsl --install` from an elevated
677
+ prompt and reboot if requested.
678
+ - `python3` is not available in the selected distribution: the WSL default
679
+ distribution may not be a regular Linux distribution (for example
680
+ `docker-desktop`). List distributions with `wsl -l -v` and either select a
681
+ suitable one with `--distro`, or install Python inside WSL with
682
+ `sudo apt install python3`.
683
+ - The bridge script is not readable from WSL: Windows drive automounting is
684
+ disabled. Ensure `/etc/wsl.conf` does not disable the `[automount]` section,
685
+ then restart WSL with `wsl --shutdown`.
686
+
687
+ If the helper reports that the RFC 2217 or UF2 relay port could not be bound,
688
+ another process inside WSL is holding the TCP port. com2tty attempts to clean up
689
+ leftover listeners automatically using `fuser`, which ships in the `psmisc`
690
+ package; on minimal distributions install it with `sudo apt install psmisc`, or
691
+ select a different port with `--rfc2217-port` (the UF2 relay always uses that
692
+ port plus one).
693
+
694
+ The serial-mode environment variables are written to `~/.bashrc`, to `~/.zshrc`
695
+ when zsh is detected or a `~/.zshrc` file exists, and to
696
+ `~/.config/fish/conf.d/com2tty.fish` when fish is detected. Users of other
697
+ shells must export `PLATFORMIO_UPLOAD_PORT` and `PLATFORMIO_MONITOR_PORT`
698
+ manually.
699
+
700
+ The startup banner uses ANSI colours only when standard output is an
701
+ interactive terminal that supports them; set the `NO_COLOR` environment
702
+ variable to suppress colours entirely.
703
+
704
+ If a board is not detected (the banner shows `Unknown`), its USB-UART chip is
705
+ not in the VID whitelist. Check what detection sees with `com2tty --list`, then
706
+ force the board family with `--board` (`esp32`, `pico`, `nrf52`, `samd`, or
707
+ `stm32`) so that reset and upload handling still work.
708
+
709
+ If the WSL helper reports a permission error while creating the serial symlink,
710
+ the requested path under `/dev` is not writable; the helper falls back to `/tmp`
711
+ and prints the one-time command to link the `/dev` path to it.
712
+
713
+ If the serial port reports that it is busy or access is denied, ensure no other
714
+ Windows application, such as a serial monitor or a second com2tty instance, is
715
+ holding the COM port open.
716
+
717
+ If gamepad mode reports all values as zero, Windows XInput is not receiving the
718
+ controller. Confirm the controller is not bound or attached through `usbipd`, so
719
+ that Windows owns it, and confirm it is on the expected XInput slot, which can be
720
+ changed with `--pad-index`.
721
+
722
+ If the `--uinput` tier cannot open `/dev/uinput`, complete the one-time setup
723
+ described in [Gamepad mode](#gamepad-mode). Until then, com2tty falls back to the
724
+ `/tmp` event stream.
725
+
726
+ For detailed logs and transfer statistics, run com2tty with `-d` or `--debug`.
727
+
728
+ ```cmd
729
+ com2tty COM3 --debug
730
+ ```
731
+
732
+ ## License
733
+
734
+ This project is licensed under the MIT License. See the [LICENSE](LICENSE) file
735
+ for the full text.