com2tty 0.1.3__tar.gz → 0.2.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 (36) hide show
  1. {com2tty-0.1.3/src/com2tty.egg-info → com2tty-0.2.0}/PKG-INFO +251 -50
  2. {com2tty-0.1.3 → com2tty-0.2.0}/README.md +250 -49
  3. {com2tty-0.1.3 → com2tty-0.2.0}/pyproject.toml +14 -1
  4. com2tty-0.2.0/src/com2tty/__init__.py +1 -0
  5. com2tty-0.2.0/src/com2tty/banner.py +38 -0
  6. com2tty-0.2.0/src/com2tty/boards.py +172 -0
  7. {com2tty-0.1.3 → com2tty-0.2.0}/src/com2tty/bridge.py +174 -29
  8. {com2tty-0.1.3 → com2tty-0.2.0}/src/com2tty/cli.py +84 -21
  9. com2tty-0.2.0/src/com2tty/discovery.py +72 -0
  10. {com2tty-0.1.3 → com2tty-0.2.0}/src/com2tty/host.py +405 -258
  11. {com2tty-0.1.3 → com2tty-0.2.0}/src/com2tty/pad_bridge.py +158 -5
  12. com2tty-0.2.0/src/com2tty/profiles.py +93 -0
  13. {com2tty-0.1.3 → com2tty-0.2.0}/src/com2tty/rfc2217_server.py +0 -1
  14. com2tty-0.2.0/src/com2tty/uf2.py +189 -0
  15. {com2tty-0.1.3 → com2tty-0.2.0}/src/com2tty/xinput.py +66 -1
  16. {com2tty-0.1.3 → com2tty-0.2.0/src/com2tty.egg-info}/PKG-INFO +251 -50
  17. {com2tty-0.1.3 → com2tty-0.2.0}/src/com2tty.egg-info/SOURCES.txt +8 -0
  18. com2tty-0.2.0/tests/test_boards.py +141 -0
  19. {com2tty-0.1.3 → com2tty-0.2.0}/tests/test_bridge_script.py +506 -4
  20. {com2tty-0.1.3 → com2tty-0.2.0}/tests/test_cli.py +71 -0
  21. com2tty-0.2.0/tests/test_discovery.py +136 -0
  22. {com2tty-0.1.3 → com2tty-0.2.0}/tests/test_host.py +1070 -46
  23. {com2tty-0.1.3 → com2tty-0.2.0}/tests/test_pad_bridge.py +210 -0
  24. com2tty-0.2.0/tests/test_profiles.py +160 -0
  25. {com2tty-0.1.3 → com2tty-0.2.0}/tests/test_rfc2217_server.py +30 -1
  26. {com2tty-0.1.3 → com2tty-0.2.0}/tests/test_xinput.py +78 -1
  27. com2tty-0.1.3/src/com2tty/__init__.py +0 -1
  28. {com2tty-0.1.3 → com2tty-0.2.0}/LICENSE +0 -0
  29. {com2tty-0.1.3 → com2tty-0.2.0}/setup.cfg +0 -0
  30. {com2tty-0.1.3 → com2tty-0.2.0}/setup.py +0 -0
  31. {com2tty-0.1.3 → com2tty-0.2.0}/src/com2tty/__main__.py +0 -0
  32. {com2tty-0.1.3 → com2tty-0.2.0}/src/com2tty.egg-info/dependency_links.txt +0 -0
  33. {com2tty-0.1.3 → com2tty-0.2.0}/src/com2tty.egg-info/entry_points.txt +0 -0
  34. {com2tty-0.1.3 → com2tty-0.2.0}/src/com2tty.egg-info/requires.txt +0 -0
  35. {com2tty-0.1.3 → com2tty-0.2.0}/src/com2tty.egg-info/top_level.txt +0 -0
  36. {com2tty-0.1.3 → com2tty-0.2.0}/tests/test_main.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: com2tty
3
- Version: 0.1.3
3
+ Version: 0.2.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
@@ -37,7 +37,11 @@ to Linux tools running in WSL.
37
37
  - [Installation](#installation)
38
38
  - [Configuration](#configuration)
39
39
  - [Usage](#usage)
40
+ - [Listing available ports](#listing-available-ports)
40
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)
41
45
  - [Automatic baud-rate detection](#automatic-baud-rate-detection)
42
46
  - [Configuring /dev/ttyUSB0 in WSL](#configuring-devttyusb0-in-wsl)
43
47
  - [Firmware upload through the bridge](#firmware-upload-through-the-bridge)
@@ -53,8 +57,11 @@ to Linux tools running in WSL.
53
57
  The Windows host requires Python 3.8 or later. The `pyserial` package, version
54
58
  3.5 or later, is the only runtime dependency and is installed automatically with
55
59
  the package. A working WSL installation is required, and the WSL distribution
56
- must provide `python3` on its `PATH`. The WSL helper uses only the Python
57
- standard library and therefore needs no additional packages inside WSL.
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.
58
65
 
59
66
  Serial forwarding requires a COM port that Windows can open. Gamepad forwarding
60
67
  requires a controller that the Windows XInput driver recognises, which is the
@@ -89,41 +96,59 @@ is not on your `PATH`, the package can also be invoked as a module with
89
96
 
90
97
  ## Configuration
91
98
 
92
- `com2tty` is configured entirely through command-line arguments. There are no
93
- configuration files and no environment variables that the tool itself reads.
94
- Note that in serial mode the WSL helper writes environment variables into the
95
- WSL user's `~/.bashrc`; this behaviour is described in
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
96
104
  [Firmware upload through the bridge](#firmware-upload-through-the-bridge).
97
105
 
98
- The first positional argument is the COM port. It is required in serial mode and
99
- is omitted in gamepad mode, which is selected with `--gamepad`.
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.
100
110
 
101
111
  ### Options common to both modes
102
112
 
103
- The following option applies to both modes.
113
+ The following options apply to both modes.
104
114
 
105
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.
106
120
  -d, --debug Enable verbose debug logging on standard error.
121
+ --distro NAME WSL distribution to use (default: the WSL default
122
+ distribution). Useful when the default distribution
123
+ lacks python3, for example docker-desktop.
107
124
  ```
108
125
 
109
126
  ### Serial-mode options
110
127
 
111
128
  ```text
112
- port Windows COM port to bridge, for example COM3. Required
113
- unless --gamepad is given.
129
+ port [port ...] Windows COM port(s) to bridge, for example COM3, or
130
+ COM3 COM5 to bridge two ports at once. Required
131
+ unless --gamepad or --list is given.
114
132
  -b, --baud BAUD Baud rate, or the literal value "auto" to detect the
115
133
  rate Windows has configured for the port
116
134
  (default: auto; falls back to 9600 if detection fails).
117
135
  -w, --wsl-tty PATH Target symlink path created inside WSL
118
- (default: /tmp/ttyUSB0).
136
+ (default: /tmp/ttyUSB0). With multiple ports, each
137
+ additional port increments the trailing number.
119
138
  --rfc2217-port PORT TCP port for the in-WSL RFC 2217 forwarder
120
- (default: 4000). The UF2 relay uses PORT + 1.
139
+ (default: 4000). The UF2 relay uses PORT + 1; with
140
+ multiple ports, each additional port uses PORT + 2i.
121
141
  --bytesize {5,6,7,8} Serial byte size (default: 8).
122
142
  --parity {N,E,O,S,M} Parity: none, even, odd, space, or mark (default: N).
123
143
  --stopbits {1,1.5,2} Stop bits (default: 1).
124
144
  --xonxoff Enable software flow control (XON/XOFF).
125
145
  --rtscts Enable hardware flow control (RTS/CTS).
126
146
  --dsrdtr Enable hardware flow control (DSR/DTR).
147
+ --board {auto,esp32,pico,nrf52,samd,stm32,none}
148
+ Override USB VID board detection for reset and upload
149
+ handling (default: auto). Use this when a board uses a
150
+ USB-UART chip that com2tty does not recognise; "none"
151
+ disables board-specific reset sequences entirely.
127
152
  ```
128
153
 
129
154
  ### Gamepad-mode options
@@ -146,6 +171,24 @@ port Windows COM port to bridge, for example COM3. Required
146
171
  Run `com2tty` from any Windows terminal, either PowerShell or Command Prompt. The
147
172
  process runs in the foreground and is stopped with Ctrl+C.
148
173
 
174
+ ### Listing available ports
175
+
176
+ `com2tty --list` (or `-l`) enumerates every serial port Windows can see,
177
+ without requiring usbipd or administrator rights.
178
+
179
+ ```text
180
+ > com2tty --list
181
+ Device VID:PID Bus ID Serial number Board Description
182
+ ------ --------- ------ ---------------- ------- -----------------------
183
+ COM3 - - - unknown Bluetooth serial (COM3)
184
+ COM17 2E8A:F00F 1-6 98C4FFA253A63FB7 pico USB serial device (COM17)
185
+ ```
186
+
187
+ The `Bus ID` column is the USB bus location (for example `1-6` or `2-6`), read
188
+ directly from the device descriptor. The `Board` column shows the family that
189
+ VID-based detection would assign, which is the same detection the bridge uses
190
+ for reset and upload handling.
191
+
149
192
  ### Bridging a serial port
150
193
 
151
194
  Bridge `COM3` to the default WSL path `/tmp/ttyUSB0` at 115200 baud.
@@ -166,6 +209,63 @@ relayed in both directions between the Windows COM port and the WSL pseudo
166
209
  terminal. Dynamic changes that a WSL program makes to the line settings, such as
167
210
  the baud rate, are detected and applied to the underlying Windows COM port.
168
211
 
212
+ ### Hot-plug auto-reconnect
213
+
214
+ When the bridged device is unplugged, resets, or re-enumerates, the bridge
215
+ does not need to be restarted. After a short grace period for transient
216
+ errors (such as a board rebooting into its bootloader), com2tty closes the
217
+ stale Windows handle and waits for the device to come back, first under its
218
+ original COM name and then by scanning for its USB serial number, because
219
+ Windows may assign a different COM number after a replug. Once the device
220
+ reappears the bridge resumes automatically; the WSL endpoint stays in place
221
+ the whole time.
222
+
223
+ ### Bridging multiple ports
224
+
225
+ Passing several ports bridges them all from one invocation.
226
+
227
+ ```cmd
228
+ com2tty COM3 COM5
229
+ ```
230
+
231
+ Each port gets its own WSL helper and endpoint: the symlink path is derived
232
+ from `--wsl-tty` by incrementing its trailing number (`/tmp/ttyUSB0`,
233
+ `/tmp/ttyUSB1`, ...), and the RFC 2217 port is the base value plus two per
234
+ additional port (each bridge also reserves its port plus one for the UF2
235
+ relay). Only the first port writes the PlatformIO environment variables and
236
+ intercepts `picotool`, so concurrent bridges do not overwrite each other's
237
+ shell configuration; tools targeting a secondary port can use its RFC 2217
238
+ port directly.
239
+
240
+ ### Argument profiles
241
+
242
+ Frequently used argument sets can be saved as named profiles in an INI file,
243
+ either `com2tty.ini` in the current directory or `.com2tty.ini` in the user
244
+ profile directory. Keys are the long option names (dashes and underscores are
245
+ both accepted), `port` supplies the positional argument, and boolean keys
246
+ take `true` or `false`.
247
+
248
+ ```ini
249
+ [myboard]
250
+ port = COM5
251
+ baud = 115200
252
+ wsl-tty = /tmp/my_device
253
+ board = pico
254
+
255
+ [pad]
256
+ gamepad = true
257
+ uinput = true
258
+ ```
259
+
260
+ A profile is invoked with an `@` prefix, and arguments given after the token
261
+ override the profile's values.
262
+
263
+ ```cmd
264
+ com2tty @myboard
265
+ com2tty @myboard --baud 9600
266
+ com2tty @pad
267
+ ```
268
+
169
269
  ### Automatic baud-rate detection
170
270
 
171
271
  When the baud rate is left at its default value of `auto`, com2tty queries the
@@ -209,29 +309,57 @@ involves three mechanisms.
209
309
  First, the WSL helper starts an RFC 2217 forwarder that listens on
210
310
  `127.0.0.1:<rfc2217-port>` inside WSL, where the port defaults to 4000. To make
211
311
  PlatformIO use it, the helper appends environment variables to the WSL user's
212
- `~/.bashrc`: `PLATFORMIO_UPLOAD_PORT` is set to
312
+ shell startup file: `PLATFORMIO_UPLOAD_PORT` is set to
213
313
  `rfc2217://127.0.0.1:<rfc2217-port>` and `PLATFORMIO_MONITOR_PORT` is set to the
214
- serial symlink path. Because these variables are written to `~/.bashrc`, open a
215
- new WSL shell or run `source ~/.bashrc` after starting com2tty for them to take
216
- effect. The variables are removed when com2tty exits.
314
+ serial symlink path. The variables are written to `~/.bashrc`, to `~/.zshrc`
315
+ when zsh is in use or that file exists, and to
316
+ `~/.config/fish/conf.d/com2tty.fish` when fish is in use or its configuration
317
+ directory exists. Open a new WSL shell or run `source ~/.bashrc` (or
318
+ `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.
217
322
 
218
323
  Second, com2tty detects the connected board type from its USB vendor identifier
219
324
  and performs the appropriate hardware reset on the Windows side. For ESP32-class
220
325
  boards it performs the DTR and RTS auto-reset sequence to enter the download
221
- mode. For RP2040 and RP2350 boards it performs the 1200-baud touch that triggers
222
- the BOOTSEL mass-storage mode.
223
-
224
- Third, for RP2040 and RP2350 boards, com2tty intercepts the `picotool` invocation
326
+ mode. For RP2040 and RP2350 boards, and for Adafruit nRF52 boards with the UF2
327
+ bootloader, it performs the 1200-baud touch that triggers the mass-storage
328
+ bootloader mode. For Arduino Leonardo- and SAMD-class boards it performs the
329
+ same 1200-baud touch; because their bootloader re-enumerates as a separate
330
+ serial port (often with a different USB identity than the application port),
331
+ com2tty snapshots the port list before the touch, waits for the new bootloader
332
+ port to appear, opens it, and runs the upload against it over RFC 2217. When the
333
+ upload finishes and the board reboots into the application, com2tty restores and
334
+ reopens the original application port. For STM32-class boards it pulses
335
+ DTR and RTS in the conventional BOOT0/NRST wiring so the board resets around
336
+ an upload; boards flashed through ST-LINK or DFU are unaffected by the pulse.
337
+
338
+ Third, for UF2-bootloader boards (RP2040, RP2350, and nRF52), com2tty
339
+ intercepts the `picotool` invocation
225
340
  inside WSL. When PlatformIO calls `picotool` to flash a `.uf2` image, a wrapper
226
341
  transfers the image back to the Windows host over a relay that listens on
227
342
  `127.0.0.1:<rfc2217-port + 1>`. The host then triggers BOOTSEL mode, locates the
228
343
  board's mass-storage drive, verifies the transferred image against an MD5
229
344
  checksum, and writes the image to the drive. The original `picotool` is restored
230
- when com2tty exits.
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.
231
348
 
232
349
  These mechanisms operate without any additional flags. The startup banner reports
233
350
  the detected board type, the RFC 2217 port, the UF2 relay port, and the board's
234
- USB serial number.
351
+ USB serial number. If the detected board type is wrong (for example a board whose
352
+ USB-UART chip is not recognised), override it with `--board`.
353
+
354
+ The RFC 2217 forwarder and the UF2 relay listen on the loopback interface
355
+ (`127.0.0.1`) inside the WSL distribution and perform no authentication. On a
356
+ single-user machine this is not exposed to the network, but on a shared or
357
+ multi-user WSL host any local user in the same distribution could connect to
358
+ these ports during an upload. Run com2tty only on hosts you trust, and choose a
359
+ non-default `--rfc2217-port` if another local service needs the default port. To
360
+ reclaim a port left open by a previous com2tty session, the helper only
361
+ terminates processes whose command line identifies them as a com2tty bridge; an
362
+ unrelated service occupying the port is never killed.
235
363
 
236
364
  ### Gamepad mode
237
365
 
@@ -330,7 +458,8 @@ on Windows and observe the events appear in WSL.
330
458
 
331
459
  Both tiers emit the same evdev codes. Buttons are reported as `BTN_A`, `BTN_B`,
332
460
  `BTN_X`, `BTN_Y`, `BTN_TL`, `BTN_TR`, `BTN_SELECT`, `BTN_START`, `BTN_THUMBL`,
333
- and `BTN_THUMBR`. The sticks are reported as `ABS_X` and `ABS_Y` for the left
461
+ `BTN_THUMBR`, and `BTN_MODE` for the Guide (Xbox logo) button. The sticks are
462
+ reported as `ABS_X` and `ABS_Y` for the left
334
463
  stick and `ABS_RX` and `ABS_RY` for the right stick, each spanning the signed
335
464
  16-bit range. The triggers are reported as `ABS_Z` for the left trigger and
336
465
  `ABS_RZ` for the right trigger, each spanning 0 to 255. The directional pad is
@@ -338,23 +467,33 @@ reported as `ABS_HAT0X` and `ABS_HAT0Y` with values of -1, 0, or 1. The stick Y
338
467
  axes are inverted to follow the Linux convention in which pushing up produces a
339
468
  negative value.
340
469
 
470
+ In the uinput tier the virtual pad also advertises `FF_RUMBLE` force
471
+ feedback. When a game or emulator inside WSL plays a rumble effect, the
472
+ effect's magnitudes travel back through the bridge to the Windows host, which
473
+ drives the physical controller's motors through `XInputSetState`. The strong
474
+ (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.
477
+
341
478
  The forwarded signal matches a real controller at the level of these event codes,
342
479
  ranges, and resolutions, but it is not bit-for-bit identical to a controller
343
480
  driven by the kernel `xpad` driver. The timing and latency differ because the
344
481
  path is polled and piped rather than delivered by a fixed USB interrupt interval.
345
- The Guide button is not reported, because the standard XInput state query does not
346
- expose it. Force feedback is not implemented. These differences are inherent to
347
- the approach.
482
+ The Guide button is read through the undocumented `XInputGetStateEx` call and is
483
+ reported as zero on systems where only the legacy `xinput9_1_0` DLL is
484
+ available. These differences are inherent to the approach.
348
485
 
349
486
  ## Architecture overview
350
487
 
351
488
  The package is organised around a host process on Windows and a helper process
352
489
  inside WSL connected by the standard input and output streams of the helper.
353
490
 
354
- `cli.py` parses the command line and dispatches to one of two entry functions in
355
- `host.py`. In serial mode it calls `run_bridge`; in gamepad mode it calls
356
- `run_gamepad_bridge`. `__main__.py` and the console entry point both call
357
- `cli.main`, and `__init__.py` holds the package version.
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
493
+ 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.
358
497
 
359
498
  `host.py` is the Windows side. In serial mode `run_bridge` opens the COM port with
360
499
  `pyserial`, spawns the WSL helper with `wsl python3 -u bridge.py`, and runs three
@@ -363,8 +502,11 @@ relays bytes from the helper's standard output to the COM port, and one reads th
363
502
  helper's standard error. The standard error stream carries a line-oriented control
364
503
  protocol whose messages are prefixed with `[CONTROL]`; these messages drive
365
504
  dynamic serial-setting changes, the RFC 2217 session lifecycle, and the UF2 upload
366
- sequence. `host.py` also contains the board detection and reset logic and the
367
- routine that writes a transferred UF2 image to the correct Windows drive.
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.
368
510
 
369
511
  `bridge.py` is the WSL side for serial forwarding. It creates a pseudo terminal
370
512
  with `openpty`, symlinks the requested path to the pseudo-terminal slave, falling
@@ -376,13 +518,21 @@ installs the `picotool` interceptor. `rfc2217_server.py` provides the redirector
376
518
  that implements the RFC 2217 protocol for the forwarder.
377
519
 
378
520
  The gamepad path reuses the same spawn-and-pipe transport. `xinput.py` is the
379
- Windows side: it polls an XInput controller slot through `ctypes` and packs each
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
380
523
  state snapshot into a fixed 16-byte frame, sending a frame only when the state
381
524
  changes. `pad_bridge.py` is the WSL side: it parses the frames, translates them
382
525
  into evdev events, and writes them to one of two sinks. The default sink writes to
383
526
  a `/tmp` FIFO, and the opt-in sink creates a real device through `/dev/uinput`
384
527
  using raw `ioctl` calls. Both sinks share the same event-encoding code, so the
385
- byte stream they produce is identical.
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`.
532
+
533
+ For a detailed account of the control protocol, the board reset sequences, the
534
+ reconnection model, the binary frame formats, and the known hardware-unverified
535
+ behaviour, see the [architecture document](ARCHITECTURE.md).
386
536
 
387
537
  ## Development setup
388
538
 
@@ -390,13 +540,14 @@ Install the package in editable mode together with the test tools.
390
540
 
391
541
  ```bash
392
542
  pip install -e .
393
- pip install pytest pytest-cov
543
+ pip install pytest pytest-cov ruff
394
544
  ```
395
545
 
396
- Run the test suite with coverage.
546
+ Run the test suite with coverage, and the linter.
397
547
 
398
548
  ```bash
399
549
  pytest --cov=src/com2tty --cov-report=term-missing tests/
550
+ ruff check src tests scripts
400
551
  ```
401
552
 
402
553
  A passing run reports all tests passing and full line coverage for the package.
@@ -405,24 +556,74 @@ The test suite is cross-platform. The `tests/conftest.py` file substitutes a moc
405
556
  the platform-specific system calls used by the gamepad sinks are mocked so that
406
557
  the suite runs on both Windows and Linux.
407
558
 
408
- Continuous integration is defined in `.github/workflows/ci.yml`. It runs the test
409
- suite on `windows-latest` and `ubuntu-latest` against Python 3.8, 3.9, 3.10,
410
- 3.11, and 3.12, and it enforces 100 percent line coverage by running pytest with
411
- `--cov-fail-under=100`. A separate job publishes the package to PyPI on pushes to
412
- the `main` branch.
559
+ Continuous integration is defined in `.github/workflows/ci.yml`. It runs a ruff
560
+ lint job and the test suite on `windows-latest` and `ubuntu-latest` against
561
+ Python 3.8 through 3.13, and it enforces 100 percent line coverage by running
562
+ pytest with `--cov-fail-under=100`. A separate job publishes the package to PyPI
563
+ on pushes to the `main` branch.
564
+
565
+ Because CI has no WSL or serial hardware, a manual end-to-end smoke test lives
566
+ at `scripts/e2e_smoke.py`. Run it on a real Windows host with a device attached
567
+ before a release: it checks `--list`, starts a bridge, verifies the symlink and
568
+ the RFC 2217 forwarder inside WSL, and optionally verifies an echo round-trip
569
+ when the device has TX wired to RX (`--loopback`).
570
+
571
+ ```cmd
572
+ python scripts/e2e_smoke.py --port COM17
573
+ ```
413
574
 
414
575
  ## Contributing
415
576
 
416
- Base feature branches on the `develop` branch. Commit messages follow the
417
- Conventional Commits format, for example `feat(gamepad): ...` or
418
- `test(host): ...`, as established in the project history. Every change must keep
419
- the test suite passing with 100 percent line coverage on both Windows and Ubuntu
420
- across the supported Python versions, because continuous integration enforces
421
- this. Add or update tests for any behavioural change. Open pull requests against
422
- `develop`.
577
+ Base feature branches on the `develop` branch and open pull requests against it.
578
+ Commit messages follow the Conventional Commits format, for example
579
+ `feat(gamepad): ...` or `test(host): ...`, as established in the project history.
580
+ Every change must keep the test suite passing with 100 percent line coverage on
581
+ both Windows and Ubuntu across the supported Python versions, and must pass the
582
+ ruff linter, because continuous integration enforces both. Add or update tests
583
+ for any behavioural change.
584
+
585
+ The full contribution process, including how to report bugs, the commit
586
+ convention, the linting requirements, and the manual end-to-end verification
587
+ step, is documented in [CONTRIBUTING.md](CONTRIBUTING.md).
423
588
 
424
589
  ## Troubleshooting
425
590
 
591
+ At startup com2tty verifies the WSL environment and reports a specific error if
592
+ a prerequisite is missing. The checks and their remedies are:
593
+
594
+ - `wsl.exe` is not on `PATH`: install WSL with `wsl --install` from an elevated
595
+ prompt and reboot if requested.
596
+ - `python3` is not available in the selected distribution: the WSL default
597
+ distribution may not be a regular Linux distribution (for example
598
+ `docker-desktop`). List distributions with `wsl -l -v` and either select a
599
+ suitable one with `--distro`, or install Python inside WSL with
600
+ `sudo apt install python3`.
601
+ - The bridge script is not readable from WSL: Windows drive automounting is
602
+ disabled. Ensure `/etc/wsl.conf` does not disable the `[automount]` section,
603
+ then restart WSL with `wsl --shutdown`.
604
+
605
+ If the helper reports that the RFC 2217 or UF2 relay port could not be bound,
606
+ another process inside WSL is holding the TCP port. com2tty attempts to clean up
607
+ leftover listeners automatically using `fuser`, which ships in the `psmisc`
608
+ package; on minimal distributions install it with `sudo apt install psmisc`, or
609
+ select a different port with `--rfc2217-port` (the UF2 relay always uses that
610
+ port plus one).
611
+
612
+ The serial-mode environment variables are written to `~/.bashrc`, to `~/.zshrc`
613
+ when zsh is detected or a `~/.zshrc` file exists, and to
614
+ `~/.config/fish/conf.d/com2tty.fish` when fish is detected. Users of other
615
+ shells must export `PLATFORMIO_UPLOAD_PORT` and `PLATFORMIO_MONITOR_PORT`
616
+ manually.
617
+
618
+ The startup banner uses ANSI colours only when standard output is an
619
+ interactive terminal that supports them; set the `NO_COLOR` environment
620
+ variable to suppress colours entirely.
621
+
622
+ If a board is not detected (the banner shows `Unknown`), its USB-UART chip is
623
+ not in the VID whitelist. Check what detection sees with `com2tty --list`, then
624
+ force the board family with `--board` (`esp32`, `pico`, `nrf52`, `samd`, or
625
+ `stm32`) so that reset and upload handling still work.
626
+
426
627
  If the WSL helper reports a permission error while creating the serial symlink,
427
628
  the requested path under `/dev` is not writable; the helper falls back to `/tmp`
428
629
  and prints the one-time command to link the `/dev` path to it.