Skip to content

Commit 6a851bb

Browse files
Merge pull request #47 from JakubAndrysek/fix-testing-and-examples
Add `get_connected_devices_by_path` API to more clearly handle multiple devices of different types
2 parents f81d053 + b96f1d5 commit 6a851bb

14 files changed

Lines changed: 126 additions & 57 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Focus on backwards compatibility and user experience.
2+
Point out any API changes.
3+
Keep your responses very short.
4+
Do not summarize the changes.

README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,18 @@ pip install pyspacemouse
4242

4343
## Quick Start
4444

45+
Here we use `has_motion()` to check if any of the spatial axes have non-zero values.
46+
The optional argument is the threshold used, and is applied per-axis.
47+
4548
```python
4649
import pyspacemouse
4750

4851
# Context manager (recommended) - automatically closes device
4952
with pyspacemouse.open() as device:
5053
while True:
5154
state = device.read()
52-
print(state.x, state.y, state.z)
55+
if state.has_motion(0.01):
56+
print(state.x, state.y, state.z, state.roll, state.pitch, state.yaw)
5357
```
5458

5559
## API Reference
@@ -66,6 +70,10 @@ import pyspacemouse
6670
pyspacemouse.get_connected_devices()
6771
# Returns: ["SpaceNavigator", "SpaceMouse Pro", ...]
6872

73+
# List connected SpaceMouse devices with paths
74+
pyspacemouse.get_connected_devices_by_path()
75+
# Returns: {"/dev/hidraw0": "SpaceNavigator", ...}
76+
6977
# List all supported device types
7078
pyspacemouse.get_supported_devices()
7179
# Returns: [(name, vendor_id, product_id), ...]
@@ -138,6 +146,9 @@ mapping table. Custom
138146
### Callbacks
139147

140148
```python
149+
import pyspacemouse
150+
import time
151+
141152
# Button callback
142153
def on_button(state, buttons, pressed):
143154
print(f"Button {pressed} pressed!")
@@ -164,6 +175,7 @@ with pyspacemouse.open(
164175
) as device:
165176
while True:
166177
device.read() # Triggers callbacks
178+
time.sleep(0.001) # NOTE: avoid larger sleeps, which can cause data to buffer
167179
```
168180

169181
### Custom Axis Mapping

docs/README.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,14 +42,18 @@ pip install pyspacemouse
4242

4343
## Quick Start
4444

45+
Here we use `has_motion()` to check if any of the spatial axes have non-zero values.
46+
The optional argument is the threshold used, and is applied per-axis.
47+
4548
```python
4649
import pyspacemouse
4750

4851
# Context manager (recommended) - automatically closes device
4952
with pyspacemouse.open() as device:
5053
while True:
5154
state = device.read()
52-
print(state.x, state.y, state.z)
55+
if state.has_motion(0.01):
56+
print(state.x, state.y, state.z, state.roll, state.pitch, state.yaw)
5357
```
5458

5559
## API Reference
@@ -66,6 +70,10 @@ import pyspacemouse
6670
pyspacemouse.get_connected_devices()
6771
# Returns: ["SpaceNavigator", "SpaceMouse Pro", ...]
6872

73+
# List connected SpaceMouse devices with paths
74+
pyspacemouse.get_connected_devices_by_path()
75+
# Returns: {"/dev/hidraw0": "SpaceNavigator", ...}
76+
6977
# List all supported device types
7078
pyspacemouse.get_supported_devices()
7179
# Returns: [(name, vendor_id, product_id), ...]
@@ -138,6 +146,9 @@ mapping table. Custom
138146
### Callbacks
139147

140148
```python
149+
import pyspacemouse
150+
import time
151+
141152
# Button callback
142153
def on_button(state, buttons, pressed):
143154
print(f"Button {pressed} pressed!")
@@ -164,6 +175,7 @@ with pyspacemouse.open(
164175
) as device:
165176
while True:
166177
device.read() # Triggers callbacks
178+
time.sleep(0.001) # NOTE: avoid larger sleeps, which can cause data to buffer
167179
```
168180

169181
### Custom Axis Mapping

examples/01_basic.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44
ensures the device is properly closed when you're done.
55
"""
66

7-
import time
8-
97
import pyspacemouse
108

119
# Using context manager (recommended)
@@ -16,12 +14,8 @@
1614
while True:
1715
state = device.read()
1816

19-
if any(
20-
abs(val) > 0.01
21-
for val in [state.x, state.y, state.z, state.roll, state.pitch, state.yaw]
22-
):
17+
if state.has_motion():
2318
print(
2419
f"x={state.x:+.2f} y={state.y:+.2f} z={state.z:+.2f} "
2520
f"roll={state.roll:+.2f} pitch={state.pitch:+.2f} yaw={state.yaw:+.2f}"
2621
)
27-
time.sleep(0.01)

examples/03_multi_device.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,21 @@
1111

1212
def main():
1313
# First, discover connected devices
14-
connected = pyspacemouse.get_connected_devices()
15-
print(f"Found {len(connected)} device(s): {connected}")
14+
connected = pyspacemouse.get_connected_devices_by_path()
15+
print(f"Found {len(connected)} spacemouse device(s): {list(connected.values())}")
1616

1717
if len(connected) < 2:
1818
print("This example requires 2 SpaceMouse devices connected.")
1919
print("Tip: Use a 3Dconnexion Universal Receiver with device_index parameter")
2020
return
2121

22-
# Open two devices using device_index
23-
# device_index=0 is the first device, device_index=1 is the second
24-
device_name = connected[0]
22+
# Arbitrarily take the first two devices found
23+
path0 = list(connected.keys())[0]
24+
path1 = list(connected.keys())[1]
2525

26-
with pyspacemouse.open(device=device_name, device_index=0) as left_hand:
27-
with pyspacemouse.open(device=device_name, device_index=1) as right_hand:
26+
# Open two devices by path
27+
with pyspacemouse.open_by_path(path0) as left_hand:
28+
with pyspacemouse.open_by_path(path1) as right_hand:
2829
print(f"Left hand: {left_hand.name}")
2930
print(f"Right hand: {right_hand.name}")
3031
print()
@@ -34,11 +35,13 @@ def main():
3435
left = left_hand.read()
3536
right = right_hand.read()
3637

37-
print(
38-
f"L: x={left.x:+.2f} y={left.y:+.2f} z={left.z:+.2f} | "
39-
f"R: x={right.x:+.2f} y={right.y:+.2f} z={right.z:+.2f}"
40-
)
41-
time.sleep(0.02)
38+
if left.has_motion() or right.has_motion():
39+
print(
40+
f"Left: x={left.x:+.2f} y={left.y:+.2f} z={left.z:+.2f} | "
41+
f"Right: x={right.x:+.2f} y={right.y:+.2f} z={right.z:+.2f}"
42+
)
43+
44+
time.sleep(0.01)
4245

4346

4447
if __name__ == "__main__":

examples/04_open_by_path.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@
99
- Windows: Uses different path format
1010
"""
1111

12-
import time
13-
1412
import pyspacemouse
1513

1614

@@ -34,8 +32,11 @@ def main():
3432

3533
while True:
3634
state = device.read()
37-
print(f"x={state.x:+.2f} y={state.y:+.2f} z={state.z:+.2f}")
38-
time.sleep(0.01)
35+
if state.has_motion():
36+
print(
37+
f"x={state.x:+.2f} y={state.y:+.2f} z={state.z:+.2f} "
38+
f"r={state.roll:+.2f} p={state.pitch:+.2f} y={state.yaw:+.2f}"
39+
)
3940

4041
except FileNotFoundError as e:
4142
print(f"Device path not found: {e}")

examples/05_discovery.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,9 @@ def main():
1515

1616
# 1. List connected SpaceMouse devices
1717
print("Connected SpaceMouse devices:")
18-
connected = pyspacemouse.get_connected_devices()
18+
connected = pyspacemouse.get_connected_devices_by_path()
1919
if connected:
20-
for name in connected:
20+
for name in connected.values():
2121
print(f" ✓ {name}")
2222
else:
2323
print(" (none found)")
@@ -26,11 +26,17 @@ def main():
2626
# 2. List all supported device types
2727
print("Supported device types:")
2828
supported = pyspacemouse.get_supported_devices()
29-
for name, vid, pid in supported:
29+
for supported_name, vid, pid in supported:
3030
# Check if this device type is connected
31-
is_connected = name in connected
32-
status = "✓" if is_connected else " "
33-
print(f" [{status}] {name} (VID: {vid:#06x}, PID: {pid:#06x})")
31+
status = " "
32+
path_if_connected = ""
33+
for path, name in connected.items():
34+
if name == supported_name:
35+
status = "✓"
36+
path_if_connected = f" (path: {path})"
37+
print(
38+
f" [{status}] {supported_name} (VID: {vid:#06x}, PID: {pid:#06x}){path_if_connected}"
39+
)
3440
print()
3541

3642
# 3. List ALL HID devices (for debugging)

examples/09_custom_config.py

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,15 @@ def example_modify_existing():
2323
print(f"Available devices: {list(specs.keys())}")
2424

2525
# Get connected devices
26-
connected = pyspacemouse.get_connected_devices()
26+
connected = pyspacemouse.get_connected_devices_by_path()
2727
if not connected:
2828
print("No devices connected!")
2929
return
30+
if len(connected) > 1:
31+
print("This example only works with one device connected.")
32+
return
3033

31-
device_name = connected[0]
34+
device_name = list(connected.values())[0]
3235
print(f"Using device: {device_name}")
3336

3437
# Get base spec and create modified version
@@ -49,11 +52,11 @@ def example_modify_existing():
4952
print("Move the SpaceMouse (Ctrl+C to exit)")
5053
print("Y and Z axes are now inverted!\n")
5154

52-
for _ in range(50): # Run for ~5 seconds
55+
for _ in range(500): # Run for ~5 seconds
5356
state = device.read()
54-
if any([state.x, state.y, state.z]):
57+
if state.has_motion():
5558
print(f"x={state.x:+.2f} y={state.y:+.2f} z={state.z:+.2f} (Y/Z inverted)")
56-
time.sleep(0.1)
59+
time.sleep(0.01)
5760

5861

5962
def example_invert_rotations():
@@ -62,30 +65,34 @@ def example_invert_rotations():
6265
print("Example 2: Fix rotation conventions")
6366
print("=" * 60)
6467

65-
connected = pyspacemouse.get_connected_devices()
68+
connected = pyspacemouse.get_connected_devices_by_path()
6669
if not connected:
6770
print("No devices connected!")
6871
return
72+
if len(connected) > 1:
73+
print("This example only works with one device connected.")
74+
return
6975

76+
device_name = list(connected.values())[0]
7077
specs = pyspacemouse.get_device_specs()
71-
base_spec = specs[connected[0]]
78+
base_spec = specs[device_name]
7279

7380
# Invert roll and yaw for right-handed coordinate system
7481
fixed_spec = pyspacemouse.modify_device_info(
7582
base_spec,
76-
name=f"{connected[0]} (Fixed Rotations)",
83+
name=f"{device_name} (Fixed Rotations)",
7784
invert_axes=["roll", "yaw"],
7885
)
7986

8087
with pyspacemouse.open(device_spec=fixed_spec) as device:
8188
print(f"Connected to: {device.name}")
8289
print("Roll and Yaw are now inverted!\n")
8390

84-
for _ in range(30):
91+
for _ in range(500):
8592
state = device.read()
86-
if any([state.roll, state.pitch, state.yaw]):
93+
if state.has_motion():
8794
print(f"roll={state.roll:+.2f} pitch={state.pitch:+.2f} yaw={state.yaw:+.2f}")
88-
time.sleep(0.1)
95+
time.sleep(0.01)
8996

9097

9198
def example_create_custom():

pyspacemouse/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
from .api import (
3939
get_all_hid_devices,
4040
get_connected_devices,
41+
get_connected_devices_by_path,
4142
get_supported_devices,
4243
open,
4344
open_by_path,
@@ -98,6 +99,7 @@
9899
# API
99100
"get_all_hid_devices",
100101
"get_connected_devices",
102+
"get_connected_devices_by_path",
101103
"get_supported_devices",
102104
"open",
103105
"open_by_path",

pyspacemouse/api.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818

1919
import warnings
2020
from pathlib import Path
21-
from typing import Callable, List, Optional, Sequence, Tuple
21+
from typing import Callable, Dict, List, Optional, Sequence, Tuple
2222

2323
from easyhid import Enumeration
2424

@@ -369,3 +369,32 @@ def open_with_config(
369369
device_index=device_index,
370370
axis_convention=axis_convention,
371371
)
372+
373+
374+
def get_connected_devices_by_path() -> Dict[str, str]:
375+
"""Return the paths and names of the supported devices currently connected.
376+
377+
Returns:
378+
Dict of paths: device names (e.g., {"/dev/hidraw0": "SpaceMouse Pro"}).
379+
380+
Raises:
381+
RuntimeError: If HID API is not installed.
382+
"""
383+
try:
384+
hid = Enumeration()
385+
except AttributeError as e:
386+
raise RuntimeError(
387+
"HID API is probably not installed. See https://spacemouse.kubaandrysek.cz for details."
388+
) from e
389+
390+
device_specs = get_device_specs()
391+
devices_by_path = {}
392+
393+
# hid.find() is all connected HID devices,
394+
# device_specs is all supported Spacemouse devices.
395+
for hid_device in hid.find():
396+
for name, spec in device_specs.items():
397+
if hid_device.vendor_id == spec.vendor_id and hid_device.product_id == spec.product_id:
398+
devices_by_path[hid_device.path] = name
399+
400+
return devices_by_path

0 commit comments

Comments
 (0)