com2tty 0.3.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 (89) hide show
  1. {com2tty-0.3.0/src/com2tty.egg-info → com2tty-0.3.1}/PKG-INFO +36 -16
  2. {com2tty-0.3.0 → com2tty-0.3.1}/README.md +35 -15
  3. {com2tty-0.3.0 → com2tty-0.3.1}/pyproject.toml +1 -1
  4. com2tty-0.3.1/src/com2tty/__init__.py +1 -0
  5. {com2tty-0.3.0 → com2tty-0.3.1}/src/com2tty/bridge.py +1 -1
  6. {com2tty-0.3.0 → com2tty-0.3.1}/src/com2tty/cli/__init__.py +4 -0
  7. {com2tty-0.3.0 → com2tty-0.3.1}/src/com2tty/cli/profiles.py +9 -2
  8. {com2tty-0.3.0 → com2tty-0.3.1}/src/com2tty/core/frames.py +7 -0
  9. {com2tty-0.3.0 → com2tty-0.3.1}/src/com2tty/core/protocol.py +9 -5
  10. {com2tty-0.3.0 → com2tty-0.3.1}/src/com2tty/windows/bridge_app.py +3 -1
  11. {com2tty-0.3.0 → com2tty-0.3.1}/src/com2tty/windows/doctor.py +4 -0
  12. {com2tty-0.3.0 → com2tty-0.3.1}/src/com2tty/windows/gamepad_app.py +11 -2
  13. {com2tty-0.3.0 → com2tty-0.3.1}/src/com2tty/windows/gamepad_host.py +8 -1
  14. {com2tty-0.3.0 → com2tty-0.3.1}/src/com2tty/windows/os_hacks/autoplay.py +6 -2
  15. {com2tty-0.3.0 → com2tty-0.3.1}/src/com2tty/windows/os_hacks/console.py +10 -0
  16. {com2tty-0.3.0 → com2tty-0.3.1}/src/com2tty/windows/os_hacks/device_watcher.py +8 -0
  17. {com2tty-0.3.0 → com2tty-0.3.1}/src/com2tty/windows/os_hacks/explorer.py +32 -3
  18. {com2tty-0.3.0 → com2tty-0.3.1}/src/com2tty/windows/serial_host.py +9 -0
  19. com2tty-0.3.1/src/com2tty/windows/wsl_process.py +264 -0
  20. {com2tty-0.3.0 → com2tty-0.3.1}/src/com2tty/wsl/assets/picotool_wrapper.py.in +6 -0
  21. {com2tty-0.3.0 → com2tty-0.3.1}/src/com2tty/wsl/evdev_sink.py +20 -4
  22. {com2tty-0.3.0 → com2tty-0.3.1}/src/com2tty/wsl/integrations/shell_env.py +34 -2
  23. {com2tty-0.3.0 → com2tty-0.3.1}/src/com2tty/wsl/liveness.py +33 -5
  24. {com2tty-0.3.0 → com2tty-0.3.1}/src/com2tty/wsl/pty_manager.py +32 -3
  25. {com2tty-0.3.0 → com2tty-0.3.1}/src/com2tty/wsl/serial_app.py +78 -2
  26. {com2tty-0.3.0 → com2tty-0.3.1/src/com2tty.egg-info}/PKG-INFO +36 -16
  27. {com2tty-0.3.0 → com2tty-0.3.1}/src/com2tty.egg-info/SOURCES.txt +1 -0
  28. {com2tty-0.3.0 → com2tty-0.3.1}/tests/test_bridge_app.py +2 -2
  29. {com2tty-0.3.0 → com2tty-0.3.1}/tests/test_cli.py +9 -0
  30. {com2tty-0.3.0 → com2tty-0.3.1}/tests/test_core_protocol.py +11 -0
  31. {com2tty-0.3.0 → com2tty-0.3.1}/tests/test_evdev_sink.py +27 -8
  32. com2tty-0.3.1/tests/test_explorer.py +204 -0
  33. {com2tty-0.3.0 → com2tty-0.3.1}/tests/test_gamepad_app.py +29 -4
  34. {com2tty-0.3.0 → com2tty-0.3.1}/tests/test_liveness.py +50 -9
  35. {com2tty-0.3.0 → com2tty-0.3.1}/tests/test_profiles.py +6 -0
  36. {com2tty-0.3.0 → com2tty-0.3.1}/tests/test_pty_manager.py +46 -0
  37. {com2tty-0.3.0 → com2tty-0.3.1}/tests/test_serial_app.py +86 -3
  38. {com2tty-0.3.0 → com2tty-0.3.1}/tests/test_shell_env.py +63 -0
  39. com2tty-0.3.1/tests/test_wsl_process.py +275 -0
  40. {com2tty-0.3.0 → com2tty-0.3.1}/tests/test_xinput.py +23 -19
  41. com2tty-0.3.0/src/com2tty/__init__.py +0 -1
  42. com2tty-0.3.0/src/com2tty/windows/wsl_process.py +0 -131
  43. com2tty-0.3.0/tests/test_wsl_process.py +0 -110
  44. {com2tty-0.3.0 → com2tty-0.3.1}/LICENSE +0 -0
  45. {com2tty-0.3.0 → com2tty-0.3.1}/setup.cfg +0 -0
  46. {com2tty-0.3.0 → com2tty-0.3.1}/setup.py +0 -0
  47. {com2tty-0.3.0 → com2tty-0.3.1}/src/com2tty/__main__.py +0 -0
  48. {com2tty-0.3.0 → com2tty-0.3.1}/src/com2tty/core/__init__.py +0 -0
  49. {com2tty-0.3.0 → com2tty-0.3.1}/src/com2tty/core/boards.py +0 -0
  50. {com2tty-0.3.0 → com2tty-0.3.1}/src/com2tty/core/constants.py +0 -0
  51. {com2tty-0.3.0 → com2tty-0.3.1}/src/com2tty/pad_bridge.py +0 -0
  52. {com2tty-0.3.0 → com2tty-0.3.1}/src/com2tty/windows/__init__.py +0 -0
  53. {com2tty-0.3.0 → com2tty-0.3.1}/src/com2tty/windows/board_reset.py +0 -0
  54. {com2tty-0.3.0 → com2tty-0.3.1}/src/com2tty/windows/control_handler.py +0 -0
  55. {com2tty-0.3.0 → com2tty-0.3.1}/src/com2tty/windows/discovery.py +0 -0
  56. {com2tty-0.3.0 → com2tty-0.3.1}/src/com2tty/windows/os_hacks/__init__.py +0 -0
  57. {com2tty-0.3.0 → com2tty-0.3.1}/src/com2tty/windows/rfc2217_redirector.py +0 -0
  58. {com2tty-0.3.0 → com2tty-0.3.1}/src/com2tty/windows/uf2_flash.py +0 -0
  59. {com2tty-0.3.0 → com2tty-0.3.1}/src/com2tty/wsl/__init__.py +0 -0
  60. {com2tty-0.3.0 → com2tty-0.3.1}/src/com2tty/wsl/gamepad_app.py +0 -0
  61. {com2tty-0.3.0 → com2tty-0.3.1}/src/com2tty/wsl/integrations/__init__.py +0 -0
  62. {com2tty-0.3.0 → com2tty-0.3.1}/src/com2tty/wsl/integrations/picotool.py +0 -0
  63. {com2tty-0.3.0 → com2tty-0.3.1}/src/com2tty/wsl/servers/__init__.py +0 -0
  64. {com2tty-0.3.0 → com2tty-0.3.1}/src/com2tty/wsl/servers/base.py +0 -0
  65. {com2tty-0.3.0 → com2tty-0.3.1}/src/com2tty/wsl/servers/rfc2217_forwarder.py +0 -0
  66. {com2tty-0.3.0 → com2tty-0.3.1}/src/com2tty/wsl/servers/uf2_relay.py +0 -0
  67. {com2tty-0.3.0 → com2tty-0.3.1}/src/com2tty.egg-info/dependency_links.txt +0 -0
  68. {com2tty-0.3.0 → com2tty-0.3.1}/src/com2tty.egg-info/entry_points.txt +0 -0
  69. {com2tty-0.3.0 → com2tty-0.3.1}/src/com2tty.egg-info/requires.txt +0 -0
  70. {com2tty-0.3.0 → com2tty-0.3.1}/src/com2tty.egg-info/top_level.txt +0 -0
  71. {com2tty-0.3.0 → com2tty-0.3.1}/tests/test_autoplay.py +0 -0
  72. {com2tty-0.3.0 → com2tty-0.3.1}/tests/test_board_reset.py +0 -0
  73. {com2tty-0.3.0 → com2tty-0.3.1}/tests/test_boards.py +0 -0
  74. {com2tty-0.3.0 → com2tty-0.3.1}/tests/test_console.py +0 -0
  75. {com2tty-0.3.0 → com2tty-0.3.1}/tests/test_control_handler.py +0 -0
  76. {com2tty-0.3.0 → com2tty-0.3.1}/tests/test_core_frames.py +0 -0
  77. {com2tty-0.3.0 → com2tty-0.3.1}/tests/test_devnotify.py +0 -0
  78. {com2tty-0.3.0 → com2tty-0.3.1}/tests/test_discovery.py +0 -0
  79. {com2tty-0.3.0 → com2tty-0.3.1}/tests/test_doctor.py +0 -0
  80. {com2tty-0.3.0 → com2tty-0.3.1}/tests/test_entry_shims.py +0 -0
  81. {com2tty-0.3.0 → com2tty-0.3.1}/tests/test_main.py +0 -0
  82. {com2tty-0.3.0 → com2tty-0.3.1}/tests/test_picotool.py +0 -0
  83. {com2tty-0.3.0 → com2tty-0.3.1}/tests/test_rfc2217_forwarder.py +0 -0
  84. {com2tty-0.3.0 → com2tty-0.3.1}/tests/test_rfc2217_redirector.py +0 -0
  85. {com2tty-0.3.0 → com2tty-0.3.1}/tests/test_serial_host.py +0 -0
  86. {com2tty-0.3.0 → com2tty-0.3.1}/tests/test_uf2_flash.py +0 -0
  87. {com2tty-0.3.0 → com2tty-0.3.1}/tests/test_uf2_relay.py +0 -0
  88. {com2tty-0.3.0 → com2tty-0.3.1}/tests/test_wsl_gamepad_app.py +0 -0
  89. {com2tty-0.3.0 → com2tty-0.3.1}/tests/test_wsl_servers_base.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: com2tty
3
- Version: 0.3.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,8 @@ 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.
120
121
  --json With --list: print the port list as a JSON array
121
122
  instead of an aligned table, for scripts and IDE
122
123
  integrations.
@@ -296,6 +297,10 @@ com2tty @myboard --baud 9600
296
297
  com2tty @pad
297
298
  ```
298
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
+
299
304
  ### Automatic baud-rate detection
300
305
 
301
306
  When the baud rate is left at its default value of `auto`, com2tty queries the
@@ -348,9 +353,11 @@ when zsh is in use or that file exists, and to
348
353
  `~/.config/fish/conf.d/com2tty.fish` when fish is in use or its configuration
349
354
  directory exists. Open a new WSL shell or run `source ~/.bashrc` (or
350
355
  `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.
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.
354
361
 
355
362
  Second, com2tty detects the connected board type from its USB vendor identifier
356
363
  and performs the appropriate hardware reset on the Windows side. For ESP32-class
@@ -373,10 +380,13 @@ inside WSL. When PlatformIO calls `picotool` to flash a `.uf2` image, a wrapper
373
380
  transfers the image back to the Windows host over a relay that listens on
374
381
  `127.0.0.1:<rfc2217-port + 1>`. The host then triggers BOOTSEL mode, locates the
375
382
  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.
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.
380
390
 
381
391
  These mechanisms operate without any additional flags. The startup banner reports
382
392
  the detected board type, the RFC 2217 port, the UF2 relay port, and the board's
@@ -392,12 +402,14 @@ non-default `--rfc2217-port` if another local service needs the default port. To
392
402
  reclaim a port left open by a previous com2tty session, the helper only
393
403
  terminates processes whose command line identifies them as a com2tty bridge; an
394
404
  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.
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.
401
413
 
402
414
  ### Gamepad mode
403
415
 
@@ -423,7 +435,10 @@ privileges at run time.
423
435
  #### Default tier: the /tmp event stream
424
436
 
425
437
  The default tier writes the evdev event stream to a FIFO under `/tmp`, by default
426
- `/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.
427
442
 
428
443
  ```cmd
429
444
  com2tty --gamepad
@@ -691,6 +706,11 @@ package; on minimal distributions install it with `sudo apt install psmisc`, or
691
706
  select a different port with `--rfc2217-port` (the UF2 relay always uses that
692
707
  port plus one).
693
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
+
694
714
  The serial-mode environment variables are written to `~/.bashrc`, to `~/.zshrc`
695
715
  when zsh is detected or a `~/.zshrc` file exists, and to
696
716
  `~/.config/fish/conf.d/com2tty.fish` when fish is detected. Users of other
@@ -100,7 +100,8 @@ The following options apply to both modes.
100
100
  --version Print the com2tty version and exit.
101
101
  -l, --list List the serial ports Windows can see (device name,
102
102
  VID:PID, USB bus id, serial number, detected board,
103
- description) and exit.
103
+ description) and exit. Supplying a COM port together
104
+ with --list is an argument error.
104
105
  --json With --list: print the port list as a JSON array
105
106
  instead of an aligned table, for scripts and IDE
106
107
  integrations.
@@ -280,6 +281,10 @@ com2tty @myboard --baud 9600
280
281
  com2tty @pad
281
282
  ```
282
283
 
284
+ A literal argument that must begin with `@` is written with a doubled marker:
285
+ `@@value` is passed through as the literal `@value` and is never interpreted as
286
+ a profile reference.
287
+
283
288
  ### Automatic baud-rate detection
284
289
 
285
290
  When the baud rate is left at its default value of `auto`, com2tty queries the
@@ -332,9 +337,11 @@ when zsh is in use or that file exists, and to
332
337
  `~/.config/fish/conf.d/com2tty.fish` when fish is in use or its configuration
333
338
  directory exists. Open a new WSL shell or run `source ~/.bashrc` (or
334
339
  `source ~/.zshrc`; fish picks the snippet up automatically) after starting
335
- com2tty for them to take effect. The variables are removed when com2tty exits;
336
- if a session is killed before it can clean up, the next run removes the stale
337
- block on startup.
340
+ com2tty for them to take effect. The variables are removed when com2tty exits,
341
+ including when the console window is closed: the WSL helper is reaped together
342
+ with the host process rather than left running, and it is given the chance to
343
+ run its own cleanup before it is forced down. If a session is nonetheless killed
344
+ before it can clean up, the next run removes the stale block on startup.
338
345
 
339
346
  Second, com2tty detects the connected board type from its USB vendor identifier
340
347
  and performs the appropriate hardware reset on the Windows side. For ESP32-class
@@ -357,10 +364,13 @@ inside WSL. When PlatformIO calls `picotool` to flash a `.uf2` image, a wrapper
357
364
  transfers the image back to the Windows host over a relay that listens on
358
365
  `127.0.0.1:<rfc2217-port + 1>`. The host then triggers BOOTSEL mode, locates the
359
366
  board's mass-storage drive, verifies the transferred image against an MD5
360
- checksum, and writes the image to the drive. The original `picotool` is restored
361
- when com2tty exits; if a session is killed before it can restore it, the next run
362
- detects and reverses the leftover interception on startup, so PlatformIO uploads
363
- are not left broken.
367
+ checksum, and writes the image to the drive. Subcommands that carry no firmware
368
+ image, such as `picotool info`, `picotool reboot`, and `picotool help`, are
369
+ forwarded to the real binary unchanged, so non-flashing uses of `picotool`
370
+ continue to work while the interception is active. The original `picotool` is
371
+ restored when com2tty exits; if a session is killed before it can restore it, the
372
+ next run detects and reverses the leftover interception on startup, so PlatformIO
373
+ uploads are not left broken.
364
374
 
365
375
  These mechanisms operate without any additional flags. The startup banner reports
366
376
  the detected board type, the RFC 2217 port, the UF2 relay port, and the board's
@@ -376,12 +386,14 @@ non-default `--rfc2217-port` if another local service needs the default port. To
376
386
  reclaim a port left open by a previous com2tty session, the helper only
377
387
  terminates processes whose command line identifies them as a com2tty bridge; an
378
388
  unrelated service occupying the port is never killed. A *running* com2tty
379
- session is never killed either: each session refreshes a heartbeat marker for
380
- its ports, so a second invocation that reuses the same `--rfc2217-port` reports
381
- the conflict and leaves the first bridge intact. Two sessions can run
382
- concurrently by giving the second one a different `--rfc2217-port` and
383
- `--wsl-tty`; each session removes only its own block from the shell startup
384
- files when it exits.
389
+ session is never disturbed: before it touches any shared state, a new session
390
+ checks whether its RFC 2217 and UF2 relay ports are already bound and, if so,
391
+ refuses to start rather than overwrite the first session's shell configuration
392
+ or steal its serial endpoint. As a further guard, the WSL helper refuses to
393
+ replace a tty symlink that already points at another live session's pseudo
394
+ terminal. Two sessions can run concurrently by giving the second one a different
395
+ `--rfc2217-port` and `--wsl-tty`; each session removes only its own block from
396
+ the shell startup files when it exits.
385
397
 
386
398
  ### Gamepad mode
387
399
 
@@ -407,7 +419,10 @@ privileges at run time.
407
419
  #### Default tier: the /tmp event stream
408
420
 
409
421
  The default tier writes the evdev event stream to a FIFO under `/tmp`, by default
410
- `/tmp/com2pad0`, and requires no privileged setup.
422
+ `/tmp/com2pad0`, and requires no privileged setup. This FIFO and its
423
+ force-feedback companion (described below) are created with owner-only
424
+ permissions, so another local user on the WSL instance cannot read the input
425
+ stream or inject events into it.
411
426
 
412
427
  ```cmd
413
428
  com2tty --gamepad
@@ -675,6 +690,11 @@ package; on minimal distributions install it with `sudo apt install psmisc`, or
675
690
  select a different port with `--rfc2217-port` (the UF2 relay always uses that
676
691
  port plus one).
677
692
 
693
+ If com2tty instead refuses to start with a message that a port is already in
694
+ use, a second com2tty session is already bound to that port. Give the new session
695
+ a different `--rfc2217-port`, and a different `--wsl-tty`, to run the two
696
+ concurrently.
697
+
678
698
  The serial-mode environment variables are written to `~/.bashrc`, to `~/.zshrc`
679
699
  when zsh is detected or a `~/.zshrc` file exists, and to
680
700
  `~/.config/fish/conf.d/com2tty.fish` when fish is detected. Users of other
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "com2tty"
7
- version = "0.3.0"
7
+ version = "0.3.1"
8
8
  description = "A Windows COM port to WSL ttyUSB forwarder"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -0,0 +1 @@
1
+ __version__ = "0.3.1"
@@ -18,4 +18,4 @@ if __package__ in (None, ""): # executed as a script inside WSL
18
18
  from com2tty.wsl.serial_app import main # noqa: E402
19
19
 
20
20
  if __name__ == "__main__": # pragma: no cover
21
- main()
21
+ sys.exit(main())
@@ -232,6 +232,10 @@ def main():
232
232
  sys.exit(run_doctor(distro=parsed_args.distro,
233
233
  rfc2217_port=parsed_args.rfc2217_port))
234
234
 
235
+ if parsed_args.list_ports and parsed_args.port:
236
+ parser.error("--list does not take a COM port; remove the positional "
237
+ "argument (it would be silently ignored)")
238
+
235
239
  if parsed_args.list_ports:
236
240
  from com2tty.windows.discovery import print_port_list
237
241
  print_port_list(as_json=parsed_args.json)
@@ -93,10 +93,17 @@ def load_profile_args(name, search_paths=None):
93
93
 
94
94
 
95
95
  def expand_profiles(argv, search_paths=None):
96
- """Replace every ``@name`` token in argv with that profile's arguments."""
96
+ """Replace every ``@name`` token in argv with that profile's arguments.
97
+
98
+ A literal argument value that must begin with ``@`` can be escaped by
99
+ doubling the marker: ``@@value`` is passed through as the literal
100
+ ``@value`` and is never interpreted as a profile reference.
101
+ """
97
102
  expanded = []
98
103
  for token in argv:
99
- if token.startswith("@") and len(token) > 1:
104
+ if token.startswith("@@"):
105
+ expanded.append(token[1:])
106
+ elif token.startswith("@") and len(token) > 1:
100
107
  expanded.extend(load_profile_args(token[1:], search_paths))
101
108
  else:
102
109
  expanded.append(token)
@@ -40,6 +40,13 @@ def pack_frame(index, connected, buttons=0, lt=0, rt=0,
40
40
  lx=0, ly=0, rx=0, ry=0):
41
41
  """Build a 16-byte controller frame. Pure function, testable anywhere."""
42
42
  flags = 0x01 if connected else 0x00
43
+ # Clamp the stick axes to the signed 16-bit range. XInput already reports
44
+ # values in range, but an out-of-range caller would otherwise make
45
+ # struct.pack raise and crash the host instead of degrading gracefully
46
+ # (consistent with the masking applied to the other fields).
47
+ def _clip16(v):
48
+ return max(-32768, min(32767, v))
49
+ lx, ly, rx, ry = _clip16(lx), _clip16(ly), _clip16(rx), _clip16(ry)
43
50
  return struct.pack(
44
51
  FRAME_FORMAT, FRAME_MAGIC0, FRAME_MAGIC1, index & 0xFF, flags,
45
52
  buttons & 0xFFFF, lt & 0xFF, rt & 0xFF,
@@ -61,8 +61,8 @@ UF2_ACK_LINE = b"[CONTROL] UF2_ACK\n"
61
61
  def format_line(name, payload=None):
62
62
  """Render one control line (without trailing newline)."""
63
63
  if payload is None:
64
- return "%s %s" % (CONTROL_PREFIX, name)
65
- return "%s %s:%s" % (CONTROL_PREFIX, name, payload)
64
+ return f"{CONTROL_PREFIX} {name}"
65
+ return f"{CONTROL_PREFIX} {name}:{payload}"
66
66
 
67
67
 
68
68
  def emit(stream, name, payload=None):
@@ -114,10 +114,14 @@ class ControlDispatcher:
114
114
  # a hypothetical UF2_UPLOAD registration.
115
115
  for name in sorted(self._handlers, key=len, reverse=True):
116
116
  if body.startswith(name):
117
- payload = None
118
117
  rest = body[len(name):]
119
- if rest.startswith(":"):
120
- payload = rest[1:]
118
+ # Require an exact name match or a ``:`` payload separator
119
+ # so e.g. a hypothetical ``SETTINGS_RESET`` line is not
120
+ # misrouted to the ``SETTINGS`` handler; a non-separator
121
+ # remainder falls through to the next (shorter) candidate.
122
+ if rest != "" and not rest.startswith(":"):
123
+ continue
124
+ payload = rest[1:] if rest.startswith(":") else None
121
125
  return self._handlers[name](
122
126
  ControlMessage(name, payload, line_str))
123
127
  if self._fallback is not None:
@@ -279,7 +279,9 @@ def _print_bridge_banner(port, board_type, rfc2217_port, usb_serial, env_setup):
279
279
  print(f"{yellow} [WARNING] Environment variables injected into your WSL shell rc (~/.bashrc, ~/.zshrc){reset}")
280
280
  print(f"{yellow} Please OPEN A NEW WSL TERMINAL or run `source ~/.bashrc` (or ~/.zshrc){reset}")
281
281
  else:
282
- print(f"{cyan} Secondary bridge: PlatformIO env vars are owned by the first port.{reset}")
282
+ print(f"{cyan} Secondary bridge: PlatformIO env vars were set up once by the{reset}")
283
+ print(f"{cyan} primary (first) port -- no shell changes are needed here. Use the{reset}")
284
+ print(f"{cyan} primary bridge's terminal output for the `source` instructions.{reset}")
283
285
  print(f"{yellow}========================================================================{reset}\n")
284
286
 
285
287
 
@@ -167,6 +167,10 @@ def check_xinput(os_name=os.name):
167
167
 
168
168
  def run_doctor(distro=None, rfc2217_port=4000):
169
169
  """Run all checks, print one line per result, return the exit status."""
170
+ # Each WSL probe can block for up to 30s if WSL is cold-starting or hung;
171
+ # without this the tool looks frozen while it waits.
172
+ print("Running com2tty environment checks (WSL probes can take a few "
173
+ "seconds each if WSL is starting up)...", flush=True)
170
174
  results = [check_wsl_exe()]
171
175
  wsl_ok = results[0][0] == OK
172
176
  if wsl_ok:
@@ -99,8 +99,12 @@ def run_gamepad_bridge(pad_index=0, poll_hz=250, name="Microsoft X-Box 360 pad",
99
99
  break
100
100
  for left, right in reader.feed(data):
101
101
  src.set_rumble(left, right)
102
- except Exception:
103
- pass
102
+ except Exception as e:
103
+ # Don't die silently: without this the rumble channel just stops
104
+ # with no clue why. Shutdown-time pipe errors are expected, so
105
+ # only surface a failure while the bridge is meant to be running.
106
+ if not shutdown_event.is_set():
107
+ logging.warning(f"Gamepad rumble reader thread stopped: {e}")
104
108
 
105
109
  t_logs = threading.Thread(target=read_wsl_logs, daemon=True)
106
110
  t_out = threading.Thread(target=drain_wsl_stdout, daemon=True)
@@ -146,6 +150,11 @@ def run_gamepad_bridge(pad_index=0, poll_hz=250, name="Microsoft X-Box 360 pad",
146
150
  finally:
147
151
  shutdown_event.set()
148
152
  logging.info("Cleaning up gamepad bridge...")
153
+ # terminate_wsl_helper closes the helper's stdin, which the helper's
154
+ # select loop sees as EOF and exits on; the daemon reader threads then
155
+ # unblock when proc.stdout/stderr reach EOF. Do NOT close those read
156
+ # pipes here: closing a pipe another thread is blocked reading
157
+ # deadlocks on Windows (the reader holds the file lock).
149
158
  terminate_wsl_helper(proc)
150
159
  logging.info("Gamepad bridge stopped successfully.")
151
160
  return exit_reason
@@ -8,6 +8,7 @@ sends 6-byte rumble frames back over its stdout. Both formats are defined
8
8
  in ``com2tty.core.frames``.
9
9
  """
10
10
  import ctypes
11
+ import os
11
12
 
12
13
  from ..core.frames import ( # noqa: F401 (re-exports kept for compatibility)
13
14
  FRAME_FORMAT,
@@ -61,10 +62,16 @@ class _XINPUT_VIBRATION(ctypes.Structure):
61
62
 
62
63
 
63
64
  def _load_xinput():
65
+ # Load by absolute System32 path rather than bare name so a stray
66
+ # ``xinput1_4.dll`` in the current working directory cannot be loaded
67
+ # in preference to the system copy (DLL hijacking).
68
+ system_dir = os.path.join(
69
+ os.environ.get("SystemRoot", r"C:\Windows"), "System32")
64
70
  last_err = None
65
71
  for name in _XINPUT_DLLS:
72
+ dll_path = os.path.join(system_dir, name + ".dll")
66
73
  try:
67
- return getattr(ctypes.windll, name)
74
+ return ctypes.WinDLL(dll_path)
68
75
  except OSError as exc: # pragma: no cover - depends on host DLLs
69
76
  last_err = exc
70
77
  continue
@@ -23,8 +23,12 @@ _AUTOPLAY_VALUE_NAME = "DisableAutoplay"
23
23
 
24
24
 
25
25
  def _autoplay_marker_path():
26
- import tempfile
27
- return os.path.join(tempfile.gettempdir(), AUTOPLAY_MARKER_FILENAME)
26
+ # Keep the recovery marker in the user's private LocalAppData rather than
27
+ # the world-writable system temp dir: the static filename there is open to
28
+ # file-squatting and symlink/junction attacks that could block execution
29
+ # or feed attacker-controlled values back into the registry restore.
30
+ base = os.environ.get("LOCALAPPDATA") or os.path.expanduser("~")
31
+ return os.path.join(base, AUTOPLAY_MARKER_FILENAME)
28
32
 
29
33
 
30
34
  def _restore_autoplay_state(existed, original_value):
@@ -9,6 +9,16 @@ def enable_vt_mode():
9
9
  try:
10
10
  import ctypes
11
11
  kernel32 = ctypes.windll.kernel32
12
+ # GetStdHandle returns a HANDLE (pointer-sized); without an explicit
13
+ # restype ctypes truncates it to a 32-bit int before it is handed to
14
+ # Get/SetConsoleMode.
15
+ kernel32.GetStdHandle.argtypes = [ctypes.c_uint32]
16
+ kernel32.GetStdHandle.restype = ctypes.c_void_p
17
+ kernel32.GetConsoleMode.argtypes = [
18
+ ctypes.c_void_p, ctypes.POINTER(ctypes.c_uint32)]
19
+ kernel32.GetConsoleMode.restype = ctypes.c_int # BOOL
20
+ kernel32.SetConsoleMode.argtypes = [ctypes.c_void_p, ctypes.c_uint32]
21
+ kernel32.SetConsoleMode.restype = ctypes.c_int # BOOL
12
22
  handle = kernel32.GetStdHandle(-11) # STD_OUTPUT_HANDLE
13
23
  mode = ctypes.c_uint32()
14
24
  if not kernel32.GetConsoleMode(handle, ctypes.byref(mode)):
@@ -75,6 +75,14 @@ class DeviceChangeWatcher:
75
75
  user32 = ctypes.windll.user32
76
76
  user32.CreateWindowExW.restype = ctypes.c_void_p
77
77
  user32.RegisterDeviceNotificationW.restype = ctypes.c_void_p
78
+ # DefWindowProcW returns an LRESULT (pointer-sized). Without an
79
+ # explicit restype ctypes truncates it to a 32-bit int, so the
80
+ # value the window procedure hands back to the OS is corrupted on
81
+ # 64-bit Windows.
82
+ user32.DefWindowProcW.restype = ctypes.c_ssize_t
83
+ user32.DefWindowProcW.argtypes = [
84
+ ctypes.c_void_p, ctypes.c_uint,
85
+ ctypes.c_void_p, ctypes.c_void_p]
78
86
 
79
87
  hwnd = user32.CreateWindowExW(
80
88
  0, "STATIC", "com2tty-devnotify", 0, 0, 0, 0, 0,
@@ -18,8 +18,31 @@ from ...core.constants import EXPLORER_WINDOW_CLASS, UF2_VOLUME_LABELS
18
18
  WM_CLOSE = 0x0010
19
19
  SW_HIDE = 0
20
20
 
21
- #: How often the background closer re-scans the desktop window list.
22
- CLOSER_SCAN_INTERVAL = 0.01
21
+ #: How often the background closer re-scans the desktop window list. 100 ms
22
+ #: is imperceptible to the user but enumerating every top-level window at
23
+ #: 100 Hz burned noticeable CPU, especially with many windows open.
24
+ CLOSER_SCAN_INTERVAL = 0.1
25
+
26
+
27
+ def _declare_window_apis(ctypes, EnumWindows, EnumWindowsProc, GetClassNameW,
28
+ GetWindowTextW, ShowWindow, PostMessageW):
29
+ """Pin the user32 signatures so 64-bit HWNDs are not truncated.
30
+
31
+ Window handles are pointer-sized; passed through ctypes' default 32-bit
32
+ int argument type they would be clipped on 64-bit Windows, addressing the
33
+ wrong (or no) window.
34
+ """
35
+ EnumWindows.argtypes = [EnumWindowsProc, ctypes.c_void_p]
36
+ EnumWindows.restype = ctypes.c_bool
37
+ GetClassNameW.argtypes = [ctypes.c_void_p, ctypes.c_wchar_p, ctypes.c_int]
38
+ GetClassNameW.restype = ctypes.c_int
39
+ GetWindowTextW.argtypes = [ctypes.c_void_p, ctypes.c_wchar_p, ctypes.c_int]
40
+ GetWindowTextW.restype = ctypes.c_int
41
+ ShowWindow.argtypes = [ctypes.c_void_p, ctypes.c_int]
42
+ ShowWindow.restype = ctypes.c_bool
43
+ PostMessageW.argtypes = [
44
+ ctypes.c_void_p, ctypes.c_uint, ctypes.c_void_p, ctypes.c_void_p]
45
+ PostMessageW.restype = ctypes.c_bool
23
46
 
24
47
 
25
48
  def close_explorer_for_drive(drive_letter):
@@ -32,6 +55,9 @@ def close_explorer_for_drive(drive_letter):
32
55
  GetWindowTextW = ctypes.windll.user32.GetWindowTextW
33
56
  ShowWindow = ctypes.windll.user32.ShowWindow
34
57
  PostMessageW = ctypes.windll.user32.PostMessageW
58
+ _declare_window_apis(ctypes, EnumWindows, EnumWindowsProc,
59
+ GetClassNameW, GetWindowTextW, ShowWindow,
60
+ PostMessageW)
35
61
 
36
62
  dl = drive_letter[0].upper()
37
63
  # Use the parenthesised "(X:)" form Explorer renders in titles; a
@@ -61,7 +87,7 @@ class BootselWindowCloser:
61
87
  """Background thread closing BOOTSEL Explorer windows as they appear.
62
88
 
63
89
  Scans every ``CLOSER_SCAN_INTERVAL`` seconds while a UF2 flash is in
64
- progress, so a window AutoPlay manages to open is hidden within ~10 ms.
90
+ progress, so a window AutoPlay manages to open is hidden within ~100 ms.
65
91
  ``target_letters`` is a *live* list: the flash routine appends the
66
92
  discovered drive letter once known, and subsequent scans match it too.
67
93
  """
@@ -90,6 +116,9 @@ class BootselWindowCloser:
90
116
  GetWindowTextW = ctypes.windll.user32.GetWindowTextW
91
117
  ShowWindow = ctypes.windll.user32.ShowWindow
92
118
  PostMessageW = ctypes.windll.user32.PostMessageW
119
+ _declare_window_apis(ctypes, EnumWindows, EnumWindowsProc,
120
+ GetClassNameW, GetWindowTextW, ShowWindow,
121
+ PostMessageW)
93
122
 
94
123
  def foreach_window(hwnd, lParam):
95
124
  class_name = ctypes.create_unicode_buffer(256)
@@ -80,7 +80,16 @@ def get_commstate_baudrate(port, _kernel32=None):
80
80
 
81
81
  try:
82
82
  kernel32 = _kernel32 if _kernel32 is not None else ctypes.windll.kernel32
83
+ # Declare the Win32 signatures so 64-bit handles/pointers are not
84
+ # truncated to ctypes' default 32-bit int.
85
+ kernel32.CreateFileW.argtypes = [
86
+ ctypes.c_wchar_p, ctypes.c_uint32, ctypes.c_uint32,
87
+ ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_void_p]
83
88
  kernel32.CreateFileW.restype = ctypes.c_void_p
89
+ kernel32.GetCommState.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
90
+ kernel32.GetCommState.restype = ctypes.c_int # BOOL
91
+ kernel32.CloseHandle.argtypes = [ctypes.c_void_p]
92
+ kernel32.CloseHandle.restype = ctypes.c_int # BOOL
84
93
  # The \\.\ prefix is required for COM10 and above, harmless below.
85
94
  handle = kernel32.CreateFileW(
86
95
  "\\\\.\\" + port, GENERIC_READ | GENERIC_WRITE, 0, None,