# Copyright (c) 2022-2026, The Mindbot Project Developers. # All rights reserved. # # SPDX-License-Identifier: BSD-3-Clause # # Adapted from XRoboToolkit (https://github.com/NVIDIA-AI-IOT/XRoboToolkit-Teleop-Sample-Python) # Original XR client by Zhigen Zhao. """XR device client for PICO 4 Ultra via XRoboToolkit SDK. This module wraps the ``xrobotoolkit_sdk`` C++ extension to provide a Pythonic interface for retrieving pose, button, joystick, and hand-tracking data from PICO XR headsets over the local network. Usage:: from xr_utils import XrClient client = XrClient() headset_pose = client.get_pose("headset") left_trigger = client.get_key_value("left_trigger") client.close() """ import numpy as np import xrobotoolkit_sdk as xrt class XrClient: """Client for PICO XR devices via the XRoboToolkit SDK.""" def __init__(self): """Initialize the SDK and start listening for XR data.""" xrt.init() print("[XrClient] XRoboToolkit SDK initialized.") # ------------------------------------------------------------------ # Pose # ------------------------------------------------------------------ def get_pose(self, name: str) -> np.ndarray: """Return the 7-DoF pose [x, y, z, qx, qy, qz, qw] of a device. Args: name: One of ``"left_controller"``, ``"right_controller"``, ``"headset"``. Returns: np.ndarray of shape (7,). """ _POSE_FUNCS = { "left_controller": xrt.get_left_controller_pose, "right_controller": xrt.get_right_controller_pose, "headset": xrt.get_headset_pose, } if name not in _POSE_FUNCS: raise ValueError( f"Invalid pose name: {name}. Choose from {list(_POSE_FUNCS)}" ) return np.asarray(_POSE_FUNCS[name](), dtype=np.float64) # ------------------------------------------------------------------ # Analog Keys (trigger / grip) # ------------------------------------------------------------------ def get_key_value(self, name: str) -> float: """Return an analog value (0.0~1.0) for a trigger or grip. Args: name: One of ``"left_trigger"``, ``"right_trigger"``, ``"left_grip"``, ``"right_grip"``. """ _KEY_FUNCS = { "left_trigger": xrt.get_left_trigger, "right_trigger": xrt.get_right_trigger, "left_grip": xrt.get_left_grip, "right_grip": xrt.get_right_grip, } if name not in _KEY_FUNCS: raise ValueError( f"Invalid key name: {name}. Choose from {list(_KEY_FUNCS)}" ) return float(_KEY_FUNCS[name]()) # ------------------------------------------------------------------ # Buttons # ------------------------------------------------------------------ def get_button(self, name: str) -> bool: """Return boolean state of a face / menu button. Args: name: One of ``"A"``, ``"B"``, ``"X"``, ``"Y"``, ``"left_menu_button"``, ``"right_menu_button"``, ``"left_axis_click"``, ``"right_axis_click"``. """ _BTN_FUNCS = { "A": xrt.get_A_button, "B": xrt.get_B_button, "X": xrt.get_X_button, "Y": xrt.get_Y_button, "left_menu_button": xrt.get_left_menu_button, "right_menu_button": xrt.get_right_menu_button, "left_axis_click": xrt.get_left_axis_click, "right_axis_click": xrt.get_right_axis_click, } if name not in _BTN_FUNCS: raise ValueError( f"Invalid button name: {name}. Choose from {list(_BTN_FUNCS)}" ) return bool(_BTN_FUNCS[name]()) # ------------------------------------------------------------------ # Joystick # ------------------------------------------------------------------ def get_joystick(self, side: str) -> np.ndarray: """Return joystick (x, y) for left or right controller. Args: side: ``"left"`` or ``"right"``. Returns: np.ndarray of shape (2,). """ if side == "left": return np.asarray(xrt.get_left_axis(), dtype=np.float64) elif side == "right": return np.asarray(xrt.get_right_axis(), dtype=np.float64) raise ValueError(f"Invalid side: {side}. Choose 'left' or 'right'.") # ------------------------------------------------------------------ # Hand Tracking # ------------------------------------------------------------------ def get_hand_tracking(self, side: str) -> np.ndarray | None: """Return hand tracking joint poses or None if inactive. Args: side: ``"left"`` or ``"right"``. Returns: np.ndarray of shape (27, 7) or None. """ if side == "left": if not xrt.get_left_hand_is_active(): return None return np.asarray(xrt.get_left_hand_tracking_state(), dtype=np.float64) elif side == "right": if not xrt.get_right_hand_is_active(): return None return np.asarray(xrt.get_right_hand_tracking_state(), dtype=np.float64) raise ValueError(f"Invalid side: {side}. Choose 'left' or 'right'.") # ------------------------------------------------------------------ # Timestamp # ------------------------------------------------------------------ def get_timestamp_ns(self) -> int: """Return the current XR system timestamp in nanoseconds.""" return int(xrt.get_time_stamp_ns()) # ------------------------------------------------------------------ # Lifecycle # ------------------------------------------------------------------ def close(self): """Shut down the SDK connection.""" xrt.close() print("[XrClient] SDK connection closed.")