Files
mindbot/scripts/environments/teleoperation/xr_utils/xr_client.py
2026-03-05 22:41:56 +08:00

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.")