You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

280 lines
11 KiB

import subprocess
import time
import numpy as np
from scipy.spatial.transform import Rotation as R
from decoupled_wbc.control.teleop.device.pico.xr_client import XrClient
from decoupled_wbc.control.teleop.streamers.base_streamer import BaseStreamer, StreamerOutput
R_HEADSET_TO_WORLD = np.array(
[
[0, 0, -1],
[-1, 0, 0],
[0, 1, 0],
]
)
class PicoStreamer(BaseStreamer):
def __init__(self):
self.xr_client = XrClient()
self.run_pico_service()
self.reset_status()
def run_pico_service(self):
# Run the pico service
self.pico_service_pid = subprocess.Popen(
["bash", "/opt/apps/roboticsservice/runService.sh"]
)
print(f"Pico service running with pid {self.pico_service_pid.pid}")
def stop_pico_service(self):
# find pid and kill it
if self.pico_service_pid:
subprocess.Popen(["kill", "-9", str(self.pico_service_pid.pid)])
print(f"Pico service killed with pid {self.pico_service_pid.pid}")
else:
print("Pico service not running")
def reset_status(self):
self.current_base_height = 0.74 # Initial base height, 0.74m (standing height)
self.toggle_policy_action_last = False
self.toggle_activation_last = False
self.toggle_data_collection_last = False
self.toggle_data_abort_last = False
def start_streaming(self):
pass
def stop_streaming(self):
self.xr_client.close()
def get(self) -> StreamerOutput:
pico_data = self._get_pico_data()
raw_data = self._generate_unified_raw_data(pico_data)
return raw_data
def __del__(self):
pass
def _get_pico_data(self):
pico_data = {}
# Get the pose of the left and right controllers and the headset
pico_data["left_pose"] = self.xr_client.get_pose_by_name("left_controller")
pico_data["right_pose"] = self.xr_client.get_pose_by_name("right_controller")
pico_data["head_pose"] = self.xr_client.get_pose_by_name("headset")
# Get key value of the left and right controllers
pico_data["left_trigger"] = self.xr_client.get_key_value_by_name("left_trigger")
pico_data["right_trigger"] = self.xr_client.get_key_value_by_name("right_trigger")
pico_data["left_grip"] = self.xr_client.get_key_value_by_name("left_grip")
pico_data["right_grip"] = self.xr_client.get_key_value_by_name("right_grip")
# Get button state of the left and right controllers
pico_data["A"] = self.xr_client.get_button_state_by_name("A")
pico_data["B"] = self.xr_client.get_button_state_by_name("B")
pico_data["X"] = self.xr_client.get_button_state_by_name("X")
pico_data["Y"] = self.xr_client.get_button_state_by_name("Y")
pico_data["left_menu_button"] = self.xr_client.get_button_state_by_name("left_menu_button")
pico_data["right_menu_button"] = self.xr_client.get_button_state_by_name(
"right_menu_button"
)
pico_data["left_axis_click"] = self.xr_client.get_button_state_by_name("left_axis_click")
pico_data["right_axis_click"] = self.xr_client.get_button_state_by_name("right_axis_click")
# Get the timestamp of the left and right controllers
pico_data["timestamp"] = self.xr_client.get_timestamp_ns()
# Get the hand tracking state of the left and right controllers
pico_data["left_hand_tracking_state"] = self.xr_client.get_hand_tracking_state("left")
pico_data["right_hand_tracking_state"] = self.xr_client.get_hand_tracking_state("right")
# Get the joystick state of the left and right controllers
pico_data["left_joystick"] = self.xr_client.get_joystick_state("left")
pico_data["right_joystick"] = self.xr_client.get_joystick_state("right")
# Get the motion tracker data
pico_data["motion_tracker_data"] = self.xr_client.get_motion_tracker_data()
# Get the body tracking data
pico_data["body_tracking_data"] = self.xr_client.get_body_tracking_data()
return pico_data
def _generate_unified_raw_data(self, pico_data):
# Get controller position and orientation in z up world frame
left_controller_T = self._process_xr_pose(pico_data["left_pose"], pico_data["head_pose"])
right_controller_T = self._process_xr_pose(pico_data["right_pose"], pico_data["head_pose"])
# Get navigation commands
DEAD_ZONE = 0.1
MAX_LINEAR_VEL = 0.5 # m/s
MAX_ANGULAR_VEL = 1.0 # rad/s
fwd_bwd_input = pico_data["left_joystick"][1]
strafe_input = -pico_data["left_joystick"][0]
yaw_input = -pico_data["right_joystick"][0]
lin_vel_x = self._apply_dead_zone(fwd_bwd_input, DEAD_ZONE) * MAX_LINEAR_VEL
lin_vel_y = self._apply_dead_zone(strafe_input, DEAD_ZONE) * MAX_LINEAR_VEL
ang_vel_z = self._apply_dead_zone(yaw_input, DEAD_ZONE) * MAX_ANGULAR_VEL
# Get base height command
height_increment = 0.01 # Small step per call when button is pressed
if pico_data["Y"]:
self.current_base_height += height_increment
elif pico_data["X"]:
self.current_base_height -= height_increment
self.current_base_height = np.clip(self.current_base_height, 0.2, 0.74)
# Get gripper commands
left_fingers = self._generate_finger_data(pico_data, "left")
right_fingers = self._generate_finger_data(pico_data, "right")
# Get activation commands
toggle_policy_action_tmp = pico_data["left_menu_button"] and (
pico_data["left_trigger"] > 0.5
)
toggle_activation_tmp = pico_data["left_menu_button"] and (pico_data["right_trigger"] > 0.5)
if self.toggle_policy_action_last != toggle_policy_action_tmp:
toggle_policy_action = toggle_policy_action_tmp
else:
toggle_policy_action = False
self.toggle_policy_action_last = toggle_policy_action_tmp
if self.toggle_activation_last != toggle_activation_tmp:
toggle_activation = toggle_activation_tmp
else:
toggle_activation = False
self.toggle_activation_last = toggle_activation_tmp
# Get data collection commands
toggle_data_collection_tmp = pico_data["A"]
toggle_data_abort_tmp = pico_data["B"]
if self.toggle_data_collection_last != toggle_data_collection_tmp:
toggle_data_collection = toggle_data_collection_tmp
else:
toggle_data_collection = False
self.toggle_data_collection_last = toggle_data_collection_tmp
if self.toggle_data_abort_last != toggle_data_abort_tmp:
toggle_data_abort = toggle_data_abort_tmp
else:
toggle_data_abort = False
self.toggle_data_abort_last = toggle_data_abort_tmp
# print(f"toggle_data_collection: {toggle_data_collection}, toggle_data_abort: {toggle_data_abort}")
return StreamerOutput(
ik_data={
"left_wrist": left_controller_T,
"right_wrist": right_controller_T,
"left_fingers": {"position": left_fingers},
"right_fingers": {"position": right_fingers},
},
control_data={
"base_height_command": self.current_base_height,
"navigate_cmd": [lin_vel_x, lin_vel_y, ang_vel_z],
"toggle_policy_action": toggle_policy_action,
},
teleop_data={
"toggle_activation": toggle_activation,
},
data_collection_data={
"toggle_data_collection": toggle_data_collection,
"toggle_data_abort": toggle_data_abort,
},
source="pico",
)
def _process_xr_pose(self, controller_pose, headset_pose):
# Convert controller pose to x, y, z, w quaternion
xr_pose_xyz = np.array(controller_pose)[:3] # x, y, z
xr_pose_quat = np.array(controller_pose)[3:] # x, y, z, w
# Handle all-zero quaternion case by using identity quaternion
if np.allclose(xr_pose_quat, 0):
xr_pose_quat = np.array([0, 0, 0, 1]) # identity quaternion: x, y, z, w
# Convert from y up to z up
xr_pose_xyz = R_HEADSET_TO_WORLD @ xr_pose_xyz
xr_pose_rotation = R.from_quat(xr_pose_quat).as_matrix()
xr_pose_rotation = R_HEADSET_TO_WORLD @ xr_pose_rotation @ R_HEADSET_TO_WORLD.T
# Convert headset pose to x, y, z, w quaternion
headset_pose_xyz = np.array(headset_pose)[:3]
headset_pose_quat = np.array(headset_pose)[3:]
if np.allclose(headset_pose_quat, 0):
headset_pose_quat = np.array([0, 0, 0, 1]) # identity quaternion: x, y, z, w
# Convert from y up to z up
headset_pose_xyz = R_HEADSET_TO_WORLD @ headset_pose_xyz
headset_pose_rotation = R.from_quat(headset_pose_quat).as_matrix()
headset_pose_rotation = R_HEADSET_TO_WORLD @ headset_pose_rotation @ R_HEADSET_TO_WORLD.T
# Calculate the delta between the controller and headset positions
xr_pose_xyz_delta = xr_pose_xyz - headset_pose_xyz
# Calculate the yaw of the headset
R_headset_to_world = R.from_matrix(headset_pose_rotation)
headset_pose_yaw = R_headset_to_world.as_euler("xyz")[2] # Extract yaw (Z-axis rotation)
inverse_yaw_rotation = R.from_euler("z", -headset_pose_yaw).as_matrix()
# Align with headset yaw to controller position delta and rotation
xr_pose_xyz_delta_compensated = inverse_yaw_rotation @ xr_pose_xyz_delta
xr_pose_rotation_compensated = inverse_yaw_rotation @ xr_pose_rotation
xr_pose_T = np.eye(4)
xr_pose_T[:3, :3] = xr_pose_rotation_compensated
xr_pose_T[:3, 3] = xr_pose_xyz_delta_compensated
return xr_pose_T
def _apply_dead_zone(self, value, dead_zone):
"""Apply dead zone and normalize."""
if abs(value) < dead_zone:
return 0.0
sign = 1 if value > 0 else -1
# Normalize the output to be between -1 and 1 after dead zone
return sign * (abs(value) - dead_zone) / (1.0 - dead_zone)
def _generate_finger_data(self, pico_data, hand):
"""Generate finger position data."""
fingertips = np.zeros([25, 4, 4])
thumb = 0
index = 5
middle = 10
ring = 15
# Control thumb based on shoulder button state (index 4 is thumb tip)
fingertips[4 + thumb, 0, 3] = 1.0 # open thumb
if not pico_data["left_menu_button"]:
if pico_data[f"{hand}_trigger"] > 0.5 and not pico_data[f"{hand}_grip"] > 0.5:
fingertips[4 + index, 0, 3] = 1.0 # close index
elif pico_data[f"{hand}_trigger"] > 0.5 and pico_data[f"{hand}_grip"] > 0.5:
fingertips[4 + middle, 0, 3] = 1.0 # close middle
elif not pico_data[f"{hand}_trigger"] > 0.5 and pico_data[f"{hand}_grip"] > 0.5:
fingertips[4 + ring, 0, 3] = 1.0 # close ring
return fingertips
if __name__ == "__main__":
# from decoupled_wbc.control.utils.debugger import wait_for_debugger
# wait_for_debugger()
streamer = PicoStreamer()
streamer.start_streaming()
while True:
raw_data = streamer.get()
print(
f"left_wrist: {raw_data.ik_data['left_wrist']}, right_wrist: {raw_data.ik_data['right_wrist']}"
)
time.sleep(0.1)