1616
1717from __future__ import annotations
1818
19+ import warnings
1920from pathlib import Path
2021from typing import Callable , List , Optional , Sequence , Tuple
2122
2223from easyhid import Enumeration
2324
2425from .callbacks import ButtonCallback , Config , DofCallback
26+ from .config_helpers import apply_axis_convention
2527from .device import SpaceMouseDevice
2628from .loader import get_device_specs
27- from .types import DeviceInfo , SpaceMouseState
29+ from .types import AxisConvention , DeviceInfo , SpaceMouseState
2830
2931
3032def get_connected_devices () -> List [str ]:
@@ -100,11 +102,33 @@ def _create_and_open_device(
100102 button_callback : Optional [Callable [[SpaceMouseState , List [int ]], None ]] = None ,
101103 button_callbacks : Optional [Sequence [ButtonCallback ]] = None ,
102104 nonblocking : bool = True ,
105+ axis_convention : Optional [AxisConvention ] = None ,
106+ is_custom_spec : bool = False ,
103107) -> SpaceMouseDevice :
104108 """Create, configure and open a SpaceMouseDevice.
105109
106110 This is a shared helper to avoid duplication between open() and open_by_path().
107111 """
112+ if is_custom_spec and axis_convention is not None :
113+ raise ValueError (
114+ "axis_convention and device_spec are mutually exclusive. "
115+ "Manually change the axis mapping in the spec if you need to."
116+ )
117+ if not is_custom_spec :
118+ axis_convention = (
119+ AxisConvention .LEGACY if axis_convention is None else AxisConvention (axis_convention )
120+ )
121+ if axis_convention == AxisConvention .LEGACY :
122+ warnings .warn (
123+ "AxisConvention.LEGACY is deprecated for built-in device specs "
124+ "and will be removed in a future release. Pass "
125+ "axis_convention=AxisConvention.HID_Z_UP for the recommended "
126+ "right-handed Z-up frame, or AxisConvention.HID for raw HID axes." ,
127+ DeprecationWarning ,
128+ stacklevel = 3 ,
129+ )
130+ spec = apply_axis_convention (spec , axis_convention )
131+
108132 mouse = SpaceMouseDevice (info = spec , device = hid_device )
109133 mouse .configure (
110134 callback = callback ,
@@ -127,6 +151,7 @@ def open_by_path(
127151 button_callbacks : Optional [Sequence [ButtonCallback ]] = None ,
128152 nonblocking : bool = True ,
129153 device_spec : Optional [DeviceInfo ] = None ,
154+ axis_convention : Optional [AxisConvention ] = None ,
130155) -> SpaceMouseDevice :
131156 """Open a SpaceMouse device by its filesystem path.
132157
@@ -143,7 +168,13 @@ def open_by_path(
143168 nonblocking: If True, use non-blocking reads (required for callbacks)
144169 device_spec: Optional custom DeviceInfo. If provided, uses this
145170 instead of looking up by VID/PID. Useful for custom
146- axis mappings or unsupported devices.
171+ axis mappings or unsupported devices. Custom specs are
172+ used exactly as provided.
173+ axis_convention: Coordinate convention for axis values. If None,
174+ uses the deprecated legacy convention for backward
175+ compatibility. Use AxisConvention.HID_Z_UP for a
176+ right-handed Z-up frame. Mutually exclusive with
177+ device_spec.
147178
148179 Returns:
149180 SpaceMouseDevice instance (use as context manager for auto-cleanup)
@@ -175,7 +206,8 @@ def open_by_path(
175206 raise FileNotFoundError (f"No HID device found at path '{ path } '." )
176207
177208 # Use provided spec or find matching device specification
178- if device_spec is not None :
209+ is_custom_spec = device_spec is not None
210+ if is_custom_spec :
179211 spec = device_spec
180212 else :
181213 all_specs = get_device_specs ()
@@ -207,6 +239,8 @@ def open_by_path(
207239 button_callback = button_callback ,
208240 button_callbacks = button_callbacks ,
209241 nonblocking = nonblocking ,
242+ axis_convention = axis_convention ,
243+ is_custom_spec = is_custom_spec ,
210244 )
211245
212246
@@ -220,6 +254,7 @@ def open(
220254 device : Optional [str ] = None ,
221255 device_index : int = 0 ,
222256 device_spec : Optional [DeviceInfo ] = None ,
257+ axis_convention : Optional [AxisConvention ] = None ,
223258) -> SpaceMouseDevice :
224259 """Open a SpaceMouse device by name or auto-detection.
225260
@@ -240,7 +275,14 @@ def open(
240275 device_spec: Optional custom DeviceInfo. If provided, uses this
241276 instead of looking up from TOML. Useful for custom
242277 axis mappings. The device/device_index are still used
243- to find the HID device.
278+ to find the HID device. Custom specs are used exactly
279+ as provided.
280+ axis_convention: Coordinate convention for axis values. If None,
281+ uses the deprecated legacy convention for backward
282+ compatibility. Use AxisConvention.HID_Z_UP for a
283+ geometrically consistent right-handed Z-up frame, or
284+ AxisConvention.HID for raw HID values (Z down).
285+ Mutually exclusive with device_spec.
244286
245287 Returns:
246288 SpaceMouseDevice instance (use as context manager for auto-cleanup)
@@ -261,8 +303,9 @@ def open(
261303 if device not in device_specs :
262304 raise ValueError (f"Unknown device: '{ device } '. Available: { list (device_specs .keys ())} " )
263305
264- # Use provided spec or get from TOML
265- spec = device_spec if device_spec is not None else device_specs [device ]
306+ # Use provided spec exactly as-is, or get from TOML and apply convention.
307+ is_custom_spec = device_spec is not None
308+ spec = device_spec if is_custom_spec else device_specs [device ]
266309
267310 # Find matching HID devices
268311 hid = Enumeration ()
@@ -291,6 +334,8 @@ def open(
291334 button_callback = button_callback ,
292335 button_callbacks = button_callbacks ,
293336 nonblocking = nonblocking ,
337+ axis_convention = axis_convention ,
338+ is_custom_spec = is_custom_spec ,
294339 )
295340
296341
@@ -299,6 +344,7 @@ def open_with_config(
299344 nonblocking : bool = True ,
300345 device : Optional [str ] = None ,
301346 device_index : int = 0 ,
347+ axis_convention : Optional [AxisConvention ] = None ,
302348) -> SpaceMouseDevice :
303349 """Open a SpaceMouse device using a Config object.
304350
@@ -307,6 +353,7 @@ def open_with_config(
307353 nonblocking: If True, use non-blocking reads
308354 device: Device name to open
309355 device_index: Which instance to open if multiple connected
356+ axis_convention: Coordinate convention for axis values (see open()).
310357
311358 Returns:
312359 SpaceMouseDevice instance (use as context manager for auto-cleanup)
@@ -320,4 +367,5 @@ def open_with_config(
320367 nonblocking = nonblocking ,
321368 device = device ,
322369 device_index = device_index ,
370+ axis_convention = axis_convention ,
323371 )
0 commit comments