File api.py
File List > pyspacemouse > api.py
Go to the documentation of this file
"""Public API for PySpaceMouse.
This module provides the main functions for discovering and opening SpaceMouse devices.
Usage:
# Simple usage with context manager
with pyspacemouse.open() as device:
while True:
state = device.read()
print(state.x, state.y, state.z)
# Open specific device by path
with pyspacemouse.open_by_path("/dev/hidraw0") as device:
state = device.read()
"""
from __future__ import annotations
from pathlib import Path
from typing import Callable, List, Optional, Sequence, Tuple
from easyhid import Enumeration
from .callbacks import ButtonCallback, Config, DofCallback
from .device import SpaceMouseDevice
from .loader import get_device_specs
from .types import DeviceInfo, SpaceMouseState
def get_connected_devices() -> List[str]:
"""Return a list of the supported devices currently connected.
Returns:
List of device names that are both supported and connected.
Empty list if no supported devices are found.
Raises:
RuntimeError: If HID API is not installed.
"""
try:
hid = Enumeration()
except AttributeError as e:
raise RuntimeError(
"HID API is probably not installed. See https://spacemouse.kubaandrysek.cz for details."
) from e
device_specs = get_device_specs()
devices = []
for hid_device in hid.find():
for name, spec in device_specs.items():
if hid_device.vendor_id == spec.vendor_id and hid_device.product_id == spec.product_id:
devices.append(name)
return devices
def get_supported_devices() -> List[Tuple[str, int, int]]:
"""Return a list of all supported device types (from configuration).
Returns:
List of tuples: (device_name, vendor_id, product_id)
"""
return [(name, spec.vendor_id, spec.product_id) for name, spec in get_device_specs().items()]
def get_all_hid_devices() -> List[Tuple[str, str, int, int]]:
"""Return a list of all HID devices connected to the system.
Returns:
List of tuples: (product_string, manufacturer_string, vendor_id, product_id)
Raises:
RuntimeError: If HID API is not installed.
"""
try:
hid = Enumeration()
except AttributeError as e:
raise RuntimeError(
"HID API is probably not installed. See https://spacemouse.kubaandrysek.cz for details."
) from e
return [
(
dev.product_string or "",
dev.manufacturer_string or "",
dev.vendor_id,
dev.product_id,
)
for dev in hid.find()
]
def _create_and_open_device(
spec: DeviceInfo,
hid_device,
callback: Optional[Callable[[SpaceMouseState], None]] = None,
dof_callback: Optional[Callable[[SpaceMouseState], None]] = None,
dof_callbacks: Optional[Sequence[DofCallback]] = None,
button_callback: Optional[Callable[[SpaceMouseState, List[int]], None]] = None,
button_callbacks: Optional[Sequence[ButtonCallback]] = None,
nonblocking: bool = True,
) -> SpaceMouseDevice:
"""Create, configure and open a SpaceMouseDevice.
This is a shared helper to avoid duplication between open() and open_by_path().
"""
mouse = SpaceMouseDevice(info=spec, device=hid_device)
mouse.configure(
callback=callback,
dof_callback=dof_callback,
dof_callbacks=dof_callbacks,
button_callback=button_callback,
button_callbacks=button_callbacks,
)
mouse.open()
hid_device.set_nonblocking(nonblocking)
return mouse
def open_by_path(
path: str | Path,
callback: Optional[Callable[[SpaceMouseState], None]] = None,
dof_callback: Optional[Callable[[SpaceMouseState], None]] = None,
dof_callbacks: Optional[Sequence[DofCallback]] = None,
button_callback: Optional[Callable[[SpaceMouseState, List[int]], None]] = None,
button_callbacks: Optional[Sequence[ButtonCallback]] = None,
nonblocking: bool = True,
device_spec: Optional[DeviceInfo] = None,
) -> SpaceMouseDevice:
"""Open a SpaceMouse device by its filesystem path.
This is mutually exclusive with open() - use this when you know the
exact device path, use open() for automatic device discovery.
Args:
path: Filesystem path to the HID device (e.g., "/dev/hidraw0")
callback: Called on every state change
dof_callback: Called on axis state changes
dof_callbacks: List of per-axis callbacks
button_callback: Called on button state changes
button_callbacks: List of per-button callbacks
nonblocking: If True, use non-blocking reads (required for callbacks)
device_spec: Optional custom DeviceInfo. If provided, uses this
instead of looking up by VID/PID. Useful for custom
axis mappings or unsupported devices.
Returns:
SpaceMouseDevice instance (use as context manager for auto-cleanup)
Raises:
FileNotFoundError: If the specified path does not exist
ValueError: If the device at path is not a supported SpaceMouse
(unless device_spec is provided)
"""
path = Path(path)
if not path.exists():
raise FileNotFoundError(f"Device path '{path}' does not exist.")
# Resolve path in case it's relative or a symlink
path = path.resolve()
# Find the HID device at this path
hid = Enumeration()
hid_device = None
for dev in hid.device_list:
dev_path = Path(dev.path).resolve()
if dev_path == path:
hid_device = dev
break
if hid_device is None:
raise FileNotFoundError(f"No HID device found at path '{path}'.")
# Use provided spec or find matching device specification
if device_spec is not None:
spec = device_spec
else:
all_specs = get_device_specs()
spec = None
for device_s in all_specs.values():
if (
hid_device.vendor_id == device_s.vendor_id
and hid_device.product_id == device_s.product_id
):
spec = device_s
break
if spec is None:
raise ValueError(
f"Device at '{path}' (VID={hid_device.vendor_id:#06x}, "
f"PID={hid_device.product_id:#06x}) is not a supported SpaceMouse. "
f"Use device_spec parameter for custom/unsupported devices."
)
print(f"{spec.name} found at {path}")
return _create_and_open_device(
spec=spec,
hid_device=hid_device,
callback=callback,
dof_callback=dof_callback,
dof_callbacks=dof_callbacks,
button_callback=button_callback,
button_callbacks=button_callbacks,
nonblocking=nonblocking,
)
def open(
callback: Optional[Callable[[SpaceMouseState], None]] = None,
dof_callback: Optional[Callable[[SpaceMouseState], None]] = None,
dof_callbacks: Optional[Sequence[DofCallback]] = None,
button_callback: Optional[Callable[[SpaceMouseState, List[int]], None]] = None,
button_callbacks: Optional[Sequence[ButtonCallback]] = None,
nonblocking: bool = True,
device: Optional[str] = None,
device_index: int = 0,
device_spec: Optional[DeviceInfo] = None,
) -> SpaceMouseDevice:
"""Open a SpaceMouse device by name or auto-detection.
Use as a context manager for automatic cleanup:
with pyspacemouse.open() as device:
state = device.read()
Args:
callback: Called on every state change
dof_callback: Called on axis state changes
dof_callbacks: List of per-axis callbacks
button_callback: Called on button state changes
button_callbacks: List of per-button callbacks
nonblocking: If True, use non-blocking reads (required for callbacks)
device: Device name to open. If None, uses first found device.
device_index: Which instance to open if multiple same devices connected
device_spec: Optional custom DeviceInfo. If provided, uses this
instead of looking up from TOML. Useful for custom
axis mappings. The device/device_index are still used
to find the HID device.
Returns:
SpaceMouseDevice instance (use as context manager for auto-cleanup)
Raises:
RuntimeError: If no device is found
ValueError: If the specified device name is not recognized
"""
device_specs = get_device_specs()
# Auto-detect device if not specified
if device is None:
connected = get_connected_devices()
if not connected:
raise RuntimeError("No connected or supported devices found.")
device = connected[0]
if device not in device_specs:
raise ValueError(f"Unknown device: '{device}'. Available: {list(device_specs.keys())}")
# Use provided spec or get from TOML
spec = device_spec if device_spec is not None else device_specs[device]
# Find matching HID devices
hid = Enumeration()
found = []
for hid_dev in hid.find():
if hid_dev.vendor_id == spec.vendor_id and hid_dev.product_id == spec.product_id:
found.append(hid_dev)
if not found:
raise RuntimeError(f"Device '{device}' not found.")
# Select device by index
if device_index >= len(found):
device_index = 0
hid_dev = found[device_index]
print(f"{device} found")
return _create_and_open_device(
spec=spec,
hid_device=hid_dev,
callback=callback,
dof_callback=dof_callback,
dof_callbacks=dof_callbacks,
button_callback=button_callback,
button_callbacks=button_callbacks,
nonblocking=nonblocking,
)
def open_with_config(
config: Config,
nonblocking: bool = True,
device: Optional[str] = None,
device_index: int = 0,
) -> SpaceMouseDevice:
"""Open a SpaceMouse device using a Config object.
Args:
config: Configuration with callback definitions
nonblocking: If True, use non-blocking reads
device: Device name to open
device_index: Which instance to open if multiple connected
Returns:
SpaceMouseDevice instance (use as context manager for auto-cleanup)
"""
return open(
callback=config.callback,
dof_callback=config.dof_callback,
dof_callbacks=config.dof_callbacks,
button_callback=config.button_callback,
button_callbacks=config.button_callbacks,
nonblocking=nonblocking,
device=device,
device_index=device_index,
)