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.
207 lines
7.9 KiB
207 lines
7.9 KiB
from contextlib import contextmanager
|
|
import time
|
|
from typing import Optional
|
|
|
|
import numpy as np
|
|
from scipy.spatial.transform import Rotation as R
|
|
|
|
from decoupled_wbc.control.base.policy import Policy
|
|
from decoupled_wbc.control.robot_model import RobotModel
|
|
from decoupled_wbc.control.teleop.teleop_retargeting_ik import TeleopRetargetingIK
|
|
from decoupled_wbc.control.teleop.teleop_streamer import TeleopStreamer
|
|
|
|
|
|
class TeleopPolicy(Policy):
|
|
"""
|
|
Robot-agnostic teleop policy.
|
|
Clean separation: IK processing vs command passing.
|
|
All robot-specific properties are abstracted through robot_model and hand_ik_solvers.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
body_control_device: str,
|
|
hand_control_device: str,
|
|
robot_model: RobotModel,
|
|
retargeting_ik: TeleopRetargetingIK,
|
|
body_streamer_ip: str = "192.168.?.?",
|
|
body_streamer_keyword: str = "shoulder",
|
|
enable_real_device: bool = True,
|
|
replay_data_path: Optional[str] = None,
|
|
replay_speed: float = 1.0,
|
|
wait_for_activation: int = 5,
|
|
activate_keyboard_listener: bool = True,
|
|
):
|
|
if activate_keyboard_listener:
|
|
from decoupled_wbc.control.utils.keyboard_dispatcher import KeyboardListenerSubscriber
|
|
|
|
self.keyboard_listener = KeyboardListenerSubscriber()
|
|
else:
|
|
self.keyboard_listener = None
|
|
|
|
self.wait_for_activation = wait_for_activation
|
|
|
|
self.teleop_streamer = TeleopStreamer(
|
|
robot_model=robot_model,
|
|
body_control_device=body_control_device,
|
|
hand_control_device=hand_control_device,
|
|
enable_real_device=enable_real_device,
|
|
body_streamer_ip=body_streamer_ip,
|
|
body_streamer_keyword=body_streamer_keyword,
|
|
replay_data_path=replay_data_path,
|
|
replay_speed=replay_speed,
|
|
)
|
|
self.robot_model = robot_model
|
|
self.retargeting_ik = retargeting_ik
|
|
self.is_active = False
|
|
|
|
self.latest_left_wrist_data = np.eye(4)
|
|
self.latest_right_wrist_data = np.eye(4)
|
|
self.latest_left_fingers_data = {"position": np.zeros((25, 4, 4))}
|
|
self.latest_right_fingers_data = {"position": np.zeros((25, 4, 4))}
|
|
|
|
def set_goal(self, goal: dict[str, any]):
|
|
# The current teleop policy doesn't take higher level commands yet.
|
|
pass
|
|
|
|
def get_action(self) -> dict[str, any]:
|
|
# Get structured data
|
|
streamer_output = self.teleop_streamer.get_streamer_data()
|
|
|
|
# Handle activation using teleop_data commands
|
|
self.check_activation(
|
|
streamer_output.teleop_data, wait_for_activation=self.wait_for_activation
|
|
)
|
|
|
|
action = {}
|
|
|
|
# Process streamer data if active
|
|
if self.is_active and streamer_output.ik_data:
|
|
body_data = streamer_output.ik_data["body_data"]
|
|
left_hand_data = streamer_output.ik_data["left_hand_data"]
|
|
right_hand_data = streamer_output.ik_data["right_hand_data"]
|
|
|
|
left_wrist_name = self.robot_model.supplemental_info.hand_frame_names["left"]
|
|
right_wrist_name = self.robot_model.supplemental_info.hand_frame_names["right"]
|
|
self.latest_left_wrist_data = body_data[left_wrist_name]
|
|
self.latest_right_wrist_data = body_data[right_wrist_name]
|
|
self.latest_left_fingers_data = left_hand_data
|
|
self.latest_right_fingers_data = right_hand_data
|
|
|
|
# TODO: This stores the same data again
|
|
ik_data = {
|
|
"body_data": body_data,
|
|
"left_hand_data": left_hand_data,
|
|
"right_hand_data": right_hand_data,
|
|
}
|
|
action["ik_data"] = ik_data
|
|
|
|
# Wrist poses (pos and quat)
|
|
# TODO: This stores the same wrist poses in two different formats
|
|
left_wrist_matrix = self.latest_left_wrist_data
|
|
right_wrist_matrix = self.latest_right_wrist_data
|
|
left_wrist_pose = np.concatenate(
|
|
[
|
|
left_wrist_matrix[:3, 3],
|
|
R.from_matrix(left_wrist_matrix[:3, :3]).as_quat(scalar_first=True),
|
|
]
|
|
)
|
|
right_wrist_pose = np.concatenate(
|
|
[
|
|
right_wrist_matrix[:3, 3],
|
|
R.from_matrix(right_wrist_matrix[:3, :3]).as_quat(scalar_first=True),
|
|
]
|
|
)
|
|
|
|
# Combine IK results with control commands (no teleop_data commands)
|
|
action.update(
|
|
{
|
|
"left_wrist": self.latest_left_wrist_data,
|
|
"right_wrist": self.latest_right_wrist_data,
|
|
"left_fingers": self.latest_left_fingers_data,
|
|
"right_fingers": self.latest_right_fingers_data,
|
|
"wrist_pose": np.concatenate([left_wrist_pose, right_wrist_pose]),
|
|
**streamer_output.control_data, # Only control & data collection commands pass through
|
|
**streamer_output.data_collection_data,
|
|
}
|
|
)
|
|
|
|
# Run retargeting IK
|
|
if "ik_data" in action:
|
|
self.retargeting_ik.set_goal(action["ik_data"])
|
|
action["target_upper_body_pose"] = self.retargeting_ik.get_action()
|
|
|
|
return action
|
|
|
|
def close(self) -> bool:
|
|
self.teleop_streamer.stop_streaming()
|
|
return True
|
|
|
|
def check_activation(self, teleop_data: dict, wait_for_activation: int = 5):
|
|
"""Activation logic only looks at teleop data commands"""
|
|
key = self.keyboard_listener.read_msg() if self.keyboard_listener else ""
|
|
toggle_activation_by_keyboard = key == "l"
|
|
reset_teleop_policy_by_keyboard = key == "k"
|
|
toggle_activation_by_teleop = teleop_data.get("toggle_activation", False)
|
|
|
|
if reset_teleop_policy_by_keyboard:
|
|
print("Resetting teleop policy")
|
|
self.reset()
|
|
|
|
if toggle_activation_by_keyboard or toggle_activation_by_teleop:
|
|
self.is_active = not self.is_active
|
|
if self.is_active:
|
|
print("Starting teleop policy")
|
|
|
|
if wait_for_activation > 0 and toggle_activation_by_keyboard:
|
|
print(f"Sleeping for {wait_for_activation} seconds before starting teleop...")
|
|
for i in range(wait_for_activation, 0, -1):
|
|
print(f"Starting in {i}...")
|
|
time.sleep(1)
|
|
|
|
# dda: calibration logic should use current IK data
|
|
self.teleop_streamer.calibrate()
|
|
print("Teleop policy calibrated")
|
|
else:
|
|
print("Stopping teleop policy")
|
|
|
|
@contextmanager
|
|
def activate(self):
|
|
try:
|
|
yield self
|
|
finally:
|
|
self.close()
|
|
|
|
def handle_keyboard_button(self, keycode):
|
|
"""
|
|
Handle keyboard input with proper state toggle.
|
|
"""
|
|
if keycode == "l":
|
|
# Toggle start state
|
|
self.is_active = not self.is_active
|
|
# Reset initialization when stopping
|
|
if not self.is_active:
|
|
self._initialized = False
|
|
if keycode == "k":
|
|
print("Resetting teleop policy")
|
|
self.reset()
|
|
|
|
def activate_policy(self, wait_for_activation: int = 5):
|
|
"""activate the teleop policy"""
|
|
self.is_active = False
|
|
self.check_activation(
|
|
teleop_data={"toggle_activation": True}, wait_for_activation=wait_for_activation
|
|
)
|
|
|
|
def reset(self, wait_for_activation: int = 5, auto_activate: bool = False):
|
|
"""Reset the teleop policy to the initial state, and re-activate it."""
|
|
self.teleop_streamer.reset()
|
|
self.retargeting_ik.reset()
|
|
self.is_active = False
|
|
self.latest_left_wrist_data = np.eye(4)
|
|
self.latest_right_wrist_data = np.eye(4)
|
|
self.latest_left_fingers_data = {"position": np.zeros((25, 4, 4))}
|
|
self.latest_right_fingers_data = {"position": np.zeros((25, 4, 4))}
|
|
|
|
if auto_activate:
|
|
self.activate_policy(wait_for_activation)
|