Skip to content

Commit 53185d6

Browse files
peterpeter
authored andcommitted
added .nonzero() API to spacemouse state
just for making printing/sleeping in examples simpler also added a warning about sleeps
1 parent 7147d32 commit 53185d6

12 files changed

Lines changed: 61 additions & 60 deletions

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ import pyspacemouse
4949
with pyspacemouse.open() as device:
5050
while True:
5151
state = device.read()
52-
print(state.x, state.y, state.z)
52+
if state.nonzero(0.01):
53+
print(state.x, state.y, state.z, state.roll, state.pitch, state.yaw)
5354
```
5455

5556
## API Reference
@@ -145,6 +146,7 @@ with pyspacemouse.open(
145146
) as device:
146147
while True:
147148
device.read() # Triggers callbacks
149+
time.sleep(0.001) # NOTE: avoid larger sleeps, which can cause data to buffer
148150
```
149151

150152
### 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.nonzero():
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/02_callbacks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,4 @@ def on_any_button(state, buttons):
4949

5050
while True:
5151
device.read() # Must call read() to process callbacks
52-
time.sleep(0.01)
52+
time.sleep(0.01) # NOTE: large sleeps can cause data to buffer

examples/03_multi_device.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,26 @@
44
useful for dual-hand control or controlling multiple robots.
55
"""
66

7-
import time
8-
97
import pyspacemouse
108

119

1210
def main():
1311
# First, discover connected devices
14-
paths, names = pyspacemouse.get_connected_paths_and_names()
15-
print(f"Found {len(names)} spacemouse device(s): {names}")
12+
connected = pyspacemouse.get_connected_paths_and_names()
13+
print(f"Found {len(connected)} spacemouse device(s): {list(connected.values())}")
1614

17-
if len(names) < 2:
15+
if len(connected) < 2:
1816
print("This example requires 2 SpaceMouse devices connected.")
1917
print("Tip: Use a 3Dconnexion Universal Receiver with device_index parameter")
2018
return
2119

20+
# Arbitrarily take the first two devices found
21+
path0 = list(connected.keys())[0]
22+
path1 = list(connected.keys())[1]
23+
2224
# Open two devices by path
23-
with pyspacemouse.open_by_path(paths[0]) as left_hand:
24-
with pyspacemouse.open_by_path(paths[1]) as right_hand:
25+
with pyspacemouse.open_by_path(path0) as left_hand:
26+
with pyspacemouse.open_by_path(path1) as right_hand:
2527
print(f"Left hand: {left_hand.name}")
2628
print(f"Right hand: {right_hand.name}")
2729
print()
@@ -31,11 +33,11 @@ def main():
3133
left = left_hand.read()
3234
right = right_hand.read()
3335

34-
print(
35-
f"Left: x={left.x:+.2f} y={left.y:+.2f} z={left.z:+.2f} | "
36-
f"Right: x={right.x:+.2f} y={right.y:+.2f} z={right.z:+.2f}"
37-
)
38-
time.sleep(0.02)
36+
if left.nonzero() or right.nonzero():
37+
print(
38+
f"Left: x={left.x:+.2f} y={left.y:+.2f} z={left.z:+.2f} | "
39+
f"Right: x={right.x:+.2f} y={right.y:+.2f} z={right.z:+.2f}"
40+
)
3941

4042

4143
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.nonzero():
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_paths_and_names()
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: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +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_paths_and_names()
2727
if not connected:
2828
print("No devices connected!")
2929
return
3030
if len(connected) > 1:
3131
print("This example only works with one device connected.")
3232
return
3333

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

3737
# Get base spec and create modified version
@@ -54,7 +54,7 @@ def example_modify_existing():
5454

5555
for _ in range(500): # Run for ~5 seconds
5656
state = device.read()
57-
if any([state.x, state.y, state.z]):
57+
if state.nonzero():
5858
print(f"x={state.x:+.2f} y={state.y:+.2f} z={state.z:+.2f} (Y/Z inverted)")
5959
time.sleep(0.01)
6060

@@ -65,15 +65,15 @@ def example_invert_rotations():
6565
print("Example 2: Fix rotation conventions")
6666
print("=" * 60)
6767

68-
connected = pyspacemouse.get_connected_devices()
68+
connected = pyspacemouse.get_connected_paths_and_names()
6969
if not connected:
7070
print("No devices connected!")
7171
return
7272
if len(connected) > 1:
7373
print("This example only works with one device connected.")
7474
return
7575

76-
device_name = connected[0]
76+
device_name = list(connected.values())[0]
7777
specs = pyspacemouse.get_device_specs()
7878
base_spec = specs[device_name]
7979

@@ -90,7 +90,7 @@ def example_invert_rotations():
9090

9191
for _ in range(500):
9292
state = device.read()
93-
if any([state.roll, state.pitch, state.yaw]):
93+
if state.nonzero():
9494
print(f"roll={state.roll:+.2f} pitch={state.pitch:+.2f} yaw={state.yaw:+.2f}")
9595
time.sleep(0.01)
9696

pyspacemouse/api.py

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from __future__ import annotations
1818

1919
from pathlib import Path
20-
from typing import Callable, List, Optional, Sequence, Tuple
20+
from typing import Callable, Dict, List, Optional, Sequence, Tuple
2121

2222
from easyhid import Enumeration
2323

@@ -256,7 +256,7 @@ def open(
256256
connected = get_connected_devices()
257257
if not connected:
258258
raise RuntimeError("No connected or supported devices found.")
259-
device = connected[0][1]
259+
device = connected[0]
260260

261261
if device not in device_specs:
262262
raise ValueError(f"Unknown device: '{device}'. Available: {list(device_specs.keys())}")
@@ -323,12 +323,11 @@ def open_with_config(
323323
)
324324

325325

326-
def get_connected_paths_and_names() -> Tuple[List[str], List[str]]:
326+
def get_connected_paths_and_names() -> Dict[str, str]:
327327
"""Return the paths and names of the supported devices currently connected.
328328
329329
Returns:
330-
Tuple of two lists: (device_paths, device_names).
331-
Tuple of empty lists if no supported devices are found.
330+
Dict of paths: device names (e.g., {"/dev/hidraw0": "SpaceMouse Pro"}).
332331
333332
Raises:
334333
RuntimeError: If HID API is not installed.
@@ -341,15 +340,13 @@ def get_connected_paths_and_names() -> Tuple[List[str], List[str]]:
341340
) from e
342341

343342
device_specs = get_device_specs()
344-
device_paths = []
345-
device_names = []
343+
devices_by_path = {}
346344

347345
# hid.find() is all connected HID devices,
348346
# device_specs is all supported Spacemouse devices.
349347
for hid_device in hid.find():
350348
for name, spec in device_specs.items():
351349
if hid_device.vendor_id == spec.vendor_id and hid_device.product_id == spec.product_id:
352-
device_paths.append(hid_device.path)
353-
device_names.append(name)
350+
devices_by_path[hid_device.path] = name
354351

355-
return device_paths, device_names
352+
return devices_by_path

pyspacemouse/pyspacemouse_cli.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ def print_version_cli():
1313

1414
def list_spacemouse_cli():
1515
"""List connected SpaceMouse devices."""
16-
devices = pyspacemouse.get_connected_devices()
17-
if devices:
16+
connected = pyspacemouse.get_connected_paths_and_names()
17+
if connected:
1818
print("Connected SpaceMouse devices:")
19-
for path, device in devices:
19+
for path, device in connected.items():
2020
print(f" - {device} ({path})")
2121
else:
2222
print("No connected SpaceMouse devices found.")
@@ -59,15 +59,11 @@ def test_connect_cli():
5959

6060
while True:
6161
state = device.read()
62-
if any(
63-
abs(val) > 0.01
64-
for val in [state.x, state.y, state.z, state.roll, state.pitch, state.yaw]
65-
):
62+
if state.nonzero():
6663
print(
6764
f"x={state.x:+.2f} y={state.y:+.2f} z={state.z:+.2f} "
6865
f"roll={state.roll:+.2f} pitch={state.pitch:+.2f} yaw={state.yaw:+.2f}"
6966
)
70-
time.sleep(0.01)
7167

7268
except RuntimeError as e:
7369
print(f"Failed to open SpaceMouse: {e}")

pyspacemouse/types.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,13 @@ def __getitem__(self, key: str) -> float:
8787
"""Allow dict-like access for backward compatibility."""
8888
return getattr(self, key)
8989

90+
def nonzero(self, threshold: float = 0.01) -> bool:
91+
"""
92+
Check if any axis value exceeds the given threshold.
93+
Used in example scripts to avoid printing zero values when the device is at rest.
94+
"""
95+
return any(abs(getattr(self, axis)) > threshold for axis in AXIS_NAMES)
96+
9097

9198
@dataclass(frozen=True, slots=True)
9299
class DeviceInfo:

0 commit comments

Comments
 (0)