170 lines
5.9 KiB
Python
170 lines
5.9 KiB
Python
# 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.")
|