muxplex-deck 0.4.1__tar.gz → 0.5.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.
- {muxplex_deck-0.4.1 → muxplex_deck-0.5.0}/PKG-INFO +18 -19
- {muxplex_deck-0.4.1 → muxplex_deck-0.5.0}/README.md +17 -18
- {muxplex_deck-0.4.1 → muxplex_deck-0.5.0}/pyproject.toml +1 -1
- {muxplex_deck-0.4.1 → muxplex_deck-0.5.0}/src/muxplex_deck/cli.py +157 -13
- muxplex_deck-0.5.0/src/muxplex_deck/hidhelp.py +390 -0
- {muxplex_deck-0.4.1 → muxplex_deck-0.5.0}/src/muxplex_deck/init_wizard.py +27 -3
- {muxplex_deck-0.4.1 → muxplex_deck-0.5.0}/src/muxplex_deck/main.py +106 -7
- {muxplex_deck-0.4.1 → muxplex_deck-0.5.0}/src/muxplex_deck/service.py +47 -20
- {muxplex_deck-0.4.1 → muxplex_deck-0.5.0}/src/muxplex_deck/statusfile.py +11 -0
- muxplex_deck-0.5.0/src/muxplex_deck/usbnode.py +109 -0
- muxplex_deck-0.5.0/src/muxplex_deck/wsl.py +221 -0
- {muxplex_deck-0.4.1 → muxplex_deck-0.5.0}/src/deck_probe/__init__.py +0 -0
- {muxplex_deck-0.4.1 → muxplex_deck-0.5.0}/src/deck_probe/__main__.py +0 -0
- {muxplex_deck-0.4.1 → muxplex_deck-0.5.0}/src/deck_probe/capabilities.py +0 -0
- {muxplex_deck-0.4.1 → muxplex_deck-0.5.0}/src/deck_probe/events.py +0 -0
- {muxplex_deck-0.4.1 → muxplex_deck-0.5.0}/src/deck_probe/main.py +0 -0
- {muxplex_deck-0.4.1 → muxplex_deck-0.5.0}/src/deck_probe/rendering.py +0 -0
- {muxplex_deck-0.4.1 → muxplex_deck-0.5.0}/src/muxplex_deck/__init__.py +0 -0
- {muxplex_deck-0.4.1 → muxplex_deck-0.5.0}/src/muxplex_deck/__main__.py +0 -0
- {muxplex_deck-0.4.1 → muxplex_deck-0.5.0}/src/muxplex_deck/attention.py +0 -0
- {muxplex_deck-0.4.1 → muxplex_deck-0.5.0}/src/muxplex_deck/config.py +0 -0
- {muxplex_deck-0.4.1 → muxplex_deck-0.5.0}/src/muxplex_deck/device.py +0 -0
- {muxplex_deck-0.4.1 → muxplex_deck-0.5.0}/src/muxplex_deck/device_real.py +0 -0
- {muxplex_deck-0.4.1 → muxplex_deck-0.5.0}/src/muxplex_deck/emulator.py +0 -0
- {muxplex_deck-0.4.1 → muxplex_deck-0.5.0}/src/muxplex_deck/focus.py +0 -0
- {muxplex_deck-0.4.1 → muxplex_deck-0.5.0}/src/muxplex_deck/interaction.py +0 -0
- {muxplex_deck-0.4.1 → muxplex_deck-0.5.0}/src/muxplex_deck/layout.py +0 -0
- {muxplex_deck-0.4.1 → muxplex_deck-0.5.0}/src/muxplex_deck/rendering.py +0 -0
- {muxplex_deck-0.4.1 → muxplex_deck-0.5.0}/src/muxplex_deck/views.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: muxplex-deck
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Hardware probe/diagnostic app for the Elgato Stream Deck+ (seed of the muxplex sidecar)
|
|
5
5
|
Author: Amplifier
|
|
6
6
|
Author-email: Amplifier <240397093+microsoft-amplifier@users.noreply.github.com>
|
|
@@ -108,10 +108,14 @@ without a udev rule granting your user access to the device (vendor id
|
|
|
108
108
|
prints a copy-pasteable remediation block (the same rule shown in
|
|
109
109
|
"Permissions" under the hardware probe section above) instead of silently
|
|
110
110
|
installing a service that won't work. It never writes to `/etc` itself --
|
|
111
|
-
only detects and reports.
|
|
111
|
+
only detects and reports. This remediation is only ever printed when udev
|
|
112
|
+
is actually running (`/run/udev/control` present) -- on WSL without
|
|
113
|
+
systemd, or in a container, it never fires anyway, so `muxplex-deck`
|
|
114
|
+
detects that and prints a different, WSL/container-appropriate fix instead
|
|
115
|
+
(see `docs/WSL.md`). Run the printed `sudo tee ... && sudo udevadm
|
|
112
116
|
control --reload-rules && sudo udevadm trigger` commands, then unplug and
|
|
113
|
-
replug the deck (or
|
|
114
|
-
restart`.
|
|
117
|
+
replug the deck (or `muxplex-deck wsl attach` under WSL) and `muxplex-deck
|
|
118
|
+
service restart`.
|
|
115
119
|
|
|
116
120
|
On Linux, `service install` also attempts `loginctl enable-linger
|
|
117
121
|
<you>` (best-effort, non-fatal) so the service keeps running after you log
|
|
@@ -159,25 +163,20 @@ uv run deck-probe
|
|
|
159
163
|
|
|
160
164
|
#### Windows + WSL
|
|
161
165
|
|
|
162
|
-
USB devices are
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
```powershell
|
|
166
|
-
usbipd list # find the Stream Deck's BUSID (VID 0fd9)
|
|
167
|
-
usbipd bind --busid <BUSID> # first time only
|
|
168
|
-
usbipd attach --wsl --busid <BUSID>
|
|
169
|
-
```
|
|
170
|
-
|
|
171
|
-
Then inside WSL, grant HID access via a udev rule (first time only):
|
|
166
|
+
USB devices are invisible to WSL until Windows hands them over. Rather
|
|
167
|
+
than driving `usbipd`/udev rules by hand for this PoC probe, install and
|
|
168
|
+
use the actual product instead -- it handles this for you:
|
|
172
169
|
|
|
173
170
|
```sh
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
# then detach/re-attach the device (usbipd detach + attach)
|
|
178
|
-
sudo apt install libhidapi-libusb0 # native HIDAPI, if not present
|
|
171
|
+
uv tool install muxplex-deck
|
|
172
|
+
muxplex-deck wsl attach # finds the deck, attaches it, tells you what's left
|
|
173
|
+
muxplex-deck doctor # diagnoses every remaining step, with real values
|
|
179
174
|
```
|
|
180
175
|
|
|
176
|
+
Neither needs the details -- but see `docs/WSL.md` if you want them (BUSID/
|
|
177
|
+
device-node churn, the `usbipd` vs `usbipd.exe` trap, durable vs per-attach
|
|
178
|
+
permission options).
|
|
179
|
+
|
|
181
180
|
**Close the Elgato Stream Deck app on Windows first** -- it holds
|
|
182
181
|
exclusive HID access. The probe prints this same guidance when it finds
|
|
183
182
|
no device.
|
|
@@ -95,10 +95,14 @@ without a udev rule granting your user access to the device (vendor id
|
|
|
95
95
|
prints a copy-pasteable remediation block (the same rule shown in
|
|
96
96
|
"Permissions" under the hardware probe section above) instead of silently
|
|
97
97
|
installing a service that won't work. It never writes to `/etc` itself --
|
|
98
|
-
only detects and reports.
|
|
98
|
+
only detects and reports. This remediation is only ever printed when udev
|
|
99
|
+
is actually running (`/run/udev/control` present) -- on WSL without
|
|
100
|
+
systemd, or in a container, it never fires anyway, so `muxplex-deck`
|
|
101
|
+
detects that and prints a different, WSL/container-appropriate fix instead
|
|
102
|
+
(see `docs/WSL.md`). Run the printed `sudo tee ... && sudo udevadm
|
|
99
103
|
control --reload-rules && sudo udevadm trigger` commands, then unplug and
|
|
100
|
-
replug the deck (or
|
|
101
|
-
restart`.
|
|
104
|
+
replug the deck (or `muxplex-deck wsl attach` under WSL) and `muxplex-deck
|
|
105
|
+
service restart`.
|
|
102
106
|
|
|
103
107
|
On Linux, `service install` also attempts `loginctl enable-linger
|
|
104
108
|
<you>` (best-effort, non-fatal) so the service keeps running after you log
|
|
@@ -146,25 +150,20 @@ uv run deck-probe
|
|
|
146
150
|
|
|
147
151
|
#### Windows + WSL
|
|
148
152
|
|
|
149
|
-
USB devices are
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
```powershell
|
|
153
|
-
usbipd list # find the Stream Deck's BUSID (VID 0fd9)
|
|
154
|
-
usbipd bind --busid <BUSID> # first time only
|
|
155
|
-
usbipd attach --wsl --busid <BUSID>
|
|
156
|
-
```
|
|
157
|
-
|
|
158
|
-
Then inside WSL, grant HID access via a udev rule (first time only):
|
|
153
|
+
USB devices are invisible to WSL until Windows hands them over. Rather
|
|
154
|
+
than driving `usbipd`/udev rules by hand for this PoC probe, install and
|
|
155
|
+
use the actual product instead -- it handles this for you:
|
|
159
156
|
|
|
160
157
|
```sh
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
# then detach/re-attach the device (usbipd detach + attach)
|
|
165
|
-
sudo apt install libhidapi-libusb0 # native HIDAPI, if not present
|
|
158
|
+
uv tool install muxplex-deck
|
|
159
|
+
muxplex-deck wsl attach # finds the deck, attaches it, tells you what's left
|
|
160
|
+
muxplex-deck doctor # diagnoses every remaining step, with real values
|
|
166
161
|
```
|
|
167
162
|
|
|
163
|
+
Neither needs the details -- but see `docs/WSL.md` if you want them (BUSID/
|
|
164
|
+
device-node churn, the `usbipd` vs `usbipd.exe` trap, durable vs per-attach
|
|
165
|
+
permission options).
|
|
166
|
+
|
|
168
167
|
**Close the Elgato Stream Deck app on Windows first** -- it holds
|
|
169
168
|
exclusive HID access. The probe prints this same guidance when it finds
|
|
170
169
|
no device.
|
|
@@ -421,16 +421,9 @@ def probe_deck_status(manager: Any) -> dict:
|
|
|
421
421
|
|
|
422
422
|
_NO_DEVICE_GUIDANCE = (
|
|
423
423
|
"No Stream Deck found. Things to check:\n"
|
|
424
|
-
" -
|
|
425
|
-
"
|
|
426
|
-
" -
|
|
427
|
-
" (vendor id 0x0fd9). Run 'muxplex-deck service install' for the exact\n"
|
|
428
|
-
" remediation block, or see AGENTS.md.\n"
|
|
429
|
-
" - WSL specifically: USB devices are not visible until attached from\n"
|
|
430
|
-
" Windows via usbipd -- in an admin PowerShell:\n"
|
|
431
|
-
" usbipd list (find the Stream Deck's BUSID)\n"
|
|
432
|
-
" usbipd bind --busid <BUSID> (first time only)\n"
|
|
433
|
-
" usbipd attach --wsl --busid <BUSID>\n"
|
|
424
|
+
" - Close the official Elgato Stream Deck app -- it holds exclusive HID access,\n"
|
|
425
|
+
" so muxplex-deck cannot open the device while it is running.\n"
|
|
426
|
+
" - Check the USB cable and try a different port.\n"
|
|
434
427
|
)
|
|
435
428
|
|
|
436
429
|
|
|
@@ -502,11 +495,13 @@ def check_hid_openable() -> tuple[str, str]:
|
|
|
502
495
|
if service_is_active():
|
|
503
496
|
return "ok", "HID: device in use by the muxplex-deck service (expected)"
|
|
504
497
|
|
|
498
|
+
from . import hidhelp
|
|
499
|
+
|
|
505
500
|
hint = ""
|
|
506
501
|
if sys.platform not in ("darwin", "win32") and not udev_rule_exists():
|
|
507
|
-
hint =
|
|
502
|
+
hint = hidhelp.HID_HINT_RUN_SERVICE_INSTALL
|
|
508
503
|
elif sys.platform not in ("darwin", "win32"):
|
|
509
|
-
hint =
|
|
504
|
+
hint = hidhelp.HID_HINT_RULE_EXISTS_BUT_STILL_FAILED
|
|
510
505
|
return "warn", f"HID: could not open device ({status['error']}).{hint}"
|
|
511
506
|
|
|
512
507
|
|
|
@@ -668,6 +663,15 @@ def doctor(config_path: str | None = None) -> int:
|
|
|
668
663
|
ca_file = config_mod._expand(raw["ca_file"])
|
|
669
664
|
checks.append(check_ca_file(ca_file))
|
|
670
665
|
|
|
666
|
+
# Environment guidance (WSL/usbipd/udev-liveness) BEFORE the device
|
|
667
|
+
# checks -- it explains why the next line is about to warn. Returns []
|
|
668
|
+
# on a healthy platform (macOS, or native Linux with udev running), so
|
|
669
|
+
# this adds no output there -- see hidhelp.explain_environment().
|
|
670
|
+
from . import hidhelp
|
|
671
|
+
|
|
672
|
+
for guidance in hidhelp.explain_environment():
|
|
673
|
+
checks.append((guidance.status, guidance.message))
|
|
674
|
+
|
|
671
675
|
checks.append(check_deck_detected(config_path))
|
|
672
676
|
checks.append(check_hid_openable())
|
|
673
677
|
|
|
@@ -702,6 +706,9 @@ def _format_device_line(device: dict[str, Any]) -> tuple[str, str]:
|
|
|
702
706
|
if not device.get("connected"):
|
|
703
707
|
return "warn", "Device: not connected"
|
|
704
708
|
caps = device.get("capabilities") or {}
|
|
709
|
+
hint = device.get("hint")
|
|
710
|
+
if not caps and hint:
|
|
711
|
+
return "warn", "Device: connected (capabilities unavailable)"
|
|
705
712
|
if not caps:
|
|
706
713
|
return "ok", "Device: connected (capabilities unavailable)"
|
|
707
714
|
touchscreen = "yes" if caps.get("has_touchscreen") else "no"
|
|
@@ -801,7 +808,15 @@ def status(config_path: str | None = None, *, as_json: bool = False) -> int:
|
|
|
801
808
|
else:
|
|
802
809
|
print_check("ok", f"Status updated {age:.0f}s ago (pid {data.get('pid', '?')})")
|
|
803
810
|
|
|
804
|
-
|
|
811
|
+
device_data = data.get("device", {})
|
|
812
|
+
print_check(*_format_device_line(device_data))
|
|
813
|
+
hint = device_data.get("hint")
|
|
814
|
+
if hint and not device_data.get("connected"):
|
|
815
|
+
# Populated by the sidecar's open-failure branch (see main.py /
|
|
816
|
+
# hidhelp.explain_open_failure) -- this is what turns a stale
|
|
817
|
+
# status file into the primary teaching surface instead of a
|
|
818
|
+
# bare "not connected" with no explanation.
|
|
819
|
+
print_check("warn", hint)
|
|
805
820
|
print_check(*_format_server_line(data.get("server", {})))
|
|
806
821
|
print_check("ok", _format_state_line(data.get("state", {})))
|
|
807
822
|
|
|
@@ -1016,6 +1031,122 @@ def update(*, force: bool = False) -> None:
|
|
|
1016
1031
|
doctor()
|
|
1017
1032
|
|
|
1018
1033
|
|
|
1034
|
+
# ---------------------------------------------------------------------------
|
|
1035
|
+
# wsl attach -- find, share-check, and attach the Stream Deck under WSL2.
|
|
1036
|
+
#
|
|
1037
|
+
# This is the ONLY command whose entire purpose is to mutate host state
|
|
1038
|
+
# (WSL_COLD_START_SPEC.md section 6.1): it's the sole caller of
|
|
1039
|
+
# `wsl.attach()`. Its deciding rationale isn't convenience -- it removes
|
|
1040
|
+
# the `usbipd` vs `usbipd.exe` trap from the recurring path entirely (a
|
|
1041
|
+
# user who never types either name cannot get the wrong binary; see
|
|
1042
|
+
# section 6.4). Never invokes `sudo` (P3) -- when a permission fix is
|
|
1043
|
+
# still needed after attaching, it prints the command, it doesn't run it.
|
|
1044
|
+
# ---------------------------------------------------------------------------
|
|
1045
|
+
|
|
1046
|
+
|
|
1047
|
+
def wsl_attach(*, vendor_id: str = "0fd9") -> int:
|
|
1048
|
+
"""`muxplex-deck wsl attach` -- attach the Stream Deck to this WSL2 distro.
|
|
1049
|
+
|
|
1050
|
+
Exit 0 on a successful attach (even if a follow-up permission fix is
|
|
1051
|
+
still needed -- the attach itself succeeded). Exit 1 for every state
|
|
1052
|
+
that stops short of attaching (not WSL, WSL1, usbipd.exe missing, not
|
|
1053
|
+
found, not shared) so scripts can branch on it.
|
|
1054
|
+
"""
|
|
1055
|
+
from . import hidhelp
|
|
1056
|
+
from . import usbnode as usbnode_mod
|
|
1057
|
+
from . import wsl as wsl_mod
|
|
1058
|
+
|
|
1059
|
+
print("\nmuxplex-deck wsl attach\n")
|
|
1060
|
+
|
|
1061
|
+
info = wsl_mod.detect()
|
|
1062
|
+
if not info.is_wsl:
|
|
1063
|
+
print_check(
|
|
1064
|
+
"fail", "Not running under WSL -- this command only makes sense there."
|
|
1065
|
+
)
|
|
1066
|
+
print()
|
|
1067
|
+
return 1
|
|
1068
|
+
print_check("ok", f"WSL{info.version} detected ({info.kernel})")
|
|
1069
|
+
|
|
1070
|
+
if info.version == 1:
|
|
1071
|
+
print_check("fail", hidhelp.W1_MESSAGE)
|
|
1072
|
+
print()
|
|
1073
|
+
return 1
|
|
1074
|
+
|
|
1075
|
+
paths = wsl_mod.find_usbipd()
|
|
1076
|
+
if paths.linux_impostor is not None and paths.windows is not None:
|
|
1077
|
+
print_check("warn", hidhelp.impostor_message(paths))
|
|
1078
|
+
|
|
1079
|
+
if paths.windows is None:
|
|
1080
|
+
print_check("warn", hidhelp.W2_MESSAGE)
|
|
1081
|
+
print()
|
|
1082
|
+
return 1
|
|
1083
|
+
print_check("ok", f"usbipd.exe: {paths.windows}")
|
|
1084
|
+
|
|
1085
|
+
devices = wsl_mod.list_devices(paths.windows, vendor_id=vendor_id)
|
|
1086
|
+
if devices is None:
|
|
1087
|
+
print_check("warn", hidhelp.W2_MESSAGE)
|
|
1088
|
+
print()
|
|
1089
|
+
return 1
|
|
1090
|
+
if not devices:
|
|
1091
|
+
print_check("warn", hidhelp.NO_MATCHING_WINDOWS_DEVICE_MESSAGE)
|
|
1092
|
+
print()
|
|
1093
|
+
return 1
|
|
1094
|
+
|
|
1095
|
+
device = devices[0]
|
|
1096
|
+
print_check(
|
|
1097
|
+
"ok",
|
|
1098
|
+
f"Found on Windows: BUSID {device.busid} {device.vid_pid} {device.description}",
|
|
1099
|
+
)
|
|
1100
|
+
|
|
1101
|
+
if device.state == "not_shared":
|
|
1102
|
+
print_check("warn", hidhelp.w4_message(device))
|
|
1103
|
+
print()
|
|
1104
|
+
return 1
|
|
1105
|
+
|
|
1106
|
+
if device.state == "unknown":
|
|
1107
|
+
print_check("warn", hidhelp.w3_unknown_state_message(device))
|
|
1108
|
+
print()
|
|
1109
|
+
return 1
|
|
1110
|
+
|
|
1111
|
+
if device.state == "shared":
|
|
1112
|
+
print_check("ok", "Shared -- attaching...")
|
|
1113
|
+
success, message = wsl_mod.attach(paths.windows, device.busid)
|
|
1114
|
+
if not success:
|
|
1115
|
+
print_check("fail", f"Attach failed: {message}")
|
|
1116
|
+
print()
|
|
1117
|
+
return 1
|
|
1118
|
+
print_check("ok", "Attached")
|
|
1119
|
+
else:
|
|
1120
|
+
print_check("ok", f"Already attached (BUSID {device.busid})")
|
|
1121
|
+
|
|
1122
|
+
node = usbnode_mod.find_usb_node(vendor_id)
|
|
1123
|
+
if node is None:
|
|
1124
|
+
print_check("warn", hidhelp.w6_message(device))
|
|
1125
|
+
print()
|
|
1126
|
+
return 1
|
|
1127
|
+
print_check("ok", f"Visible to Linux: {node.path}")
|
|
1128
|
+
|
|
1129
|
+
if not node.readable_writable:
|
|
1130
|
+
print_check("warn", hidhelp.w7_message(node))
|
|
1131
|
+
if not usbnode_mod.udev_is_live():
|
|
1132
|
+
print_check(
|
|
1133
|
+
"warn",
|
|
1134
|
+
hidhelp.u_dead_wsl_message(wsl_mod.wsl_conf_systemd_state()),
|
|
1135
|
+
)
|
|
1136
|
+
|
|
1137
|
+
print()
|
|
1138
|
+
print(" Next:")
|
|
1139
|
+
print(" muxplex-deck service restart")
|
|
1140
|
+
print(" muxplex-deck status")
|
|
1141
|
+
print()
|
|
1142
|
+
print(
|
|
1143
|
+
" The device number changes on every attach -- re-run "
|
|
1144
|
+
"`muxplex-deck wsl attach`\n after any unplug or Windows reboot."
|
|
1145
|
+
)
|
|
1146
|
+
print()
|
|
1147
|
+
return 0
|
|
1148
|
+
|
|
1149
|
+
|
|
1019
1150
|
# ---------------------------------------------------------------------------
|
|
1020
1151
|
# main
|
|
1021
1152
|
# ---------------------------------------------------------------------------
|
|
@@ -1085,6 +1216,13 @@ def main() -> None:
|
|
|
1085
1216
|
"key", nargs="?", help="Config key (omit to reset all)"
|
|
1086
1217
|
)
|
|
1087
1218
|
|
|
1219
|
+
wsl_parser = sub.add_parser("wsl", help="WSL2 USB/IP helpers")
|
|
1220
|
+
wsl_sub = wsl_parser.add_subparsers(dest="wsl_command")
|
|
1221
|
+
wsl_sub.add_parser(
|
|
1222
|
+
"attach",
|
|
1223
|
+
help="Find, share-check, and attach the Stream Deck to this WSL2 distro",
|
|
1224
|
+
)
|
|
1225
|
+
|
|
1088
1226
|
service_parser = sub.add_parser(
|
|
1089
1227
|
"service", help="Manage the muxplex-deck background service"
|
|
1090
1228
|
)
|
|
@@ -1129,6 +1267,12 @@ def main() -> None:
|
|
|
1129
1267
|
non_interactive=getattr(args, "non_interactive", False),
|
|
1130
1268
|
)
|
|
1131
1269
|
)
|
|
1270
|
+
elif args.command == "wsl":
|
|
1271
|
+
cmd = getattr(args, "wsl_command", None)
|
|
1272
|
+
if cmd == "attach":
|
|
1273
|
+
sys.exit(wsl_attach())
|
|
1274
|
+
else:
|
|
1275
|
+
wsl_parser.print_help()
|
|
1132
1276
|
elif args.command == "config":
|
|
1133
1277
|
config_path = getattr(args, "config", None)
|
|
1134
1278
|
cmd = getattr(args, "config_command", None)
|