refactor(teleop): 重命名 set_ctrl_status 为 set_teleop_enabled

启用时支持传入机械臂观测数据以同步初始位姿映射,
重构 Pika/UMI 遥操作内部状态管理。
This commit is contained in:
Vinman 2026-06-12 18:37:53 +08:00
parent 4467e19322
commit d959d82dbe
22 changed files with 507 additions and 369 deletions

View File

@ -105,7 +105,7 @@ class Transformations:
return roll, pitch, yaw return roll, pitch, yaw
@staticmethod @staticmethod
def rxryrz_to_matrix(axis_angle): def rxryrz_to_rotation_matrix(axis_angle):
""" """
将轴角向量 (rx, ry, rz) 转换为 3x3 旋转矩阵 将轴角向量 (rx, ry, rz) 转换为 3x3 旋转矩阵
输入: np.array([rx, ry, rz]) 输入: np.array([rx, ry, rz])
@ -207,6 +207,14 @@ class Transformations:
T[:3, 3] = [x, y, z] T[:3, 3] = [x, y, z]
return T return T
@classmethod
def xyzrxryrz_to_rotation_matrix(cls, x, y, z, rx, ry, rz):
"""构造4x4齐次变换矩阵"""
T = np.eye(4)
T[:3, :3] = cls.rxryrz_to_rotation_matrix([rx, ry, rz])
T[:3, 3] = [x, y, z]
return T
@classmethod @classmethod
def rotation_matrix_to_xyzq(cls, rotation_matrix): def rotation_matrix_to_xyzq(cls, rotation_matrix):
"""从4x4齐次变换矩阵到xyzq的转换""" """从4x4齐次变换矩阵到xyzq的转换"""

View File

@ -6,3 +6,7 @@ from .uf_mock_robot_config import UFMockRobotConfig
@dataclass @dataclass
class MultipleUFMockRobotConfig(RobotConfig): class MultipleUFMockRobotConfig(RobotConfig):
robots: dict[str, UFMockRobotConfig] robots: dict[str, UFMockRobotConfig]
def __post_init__(self):
super().__post_init__()
self.id = 'multiple_uf_mock_robot' if self.id is None else self.id

View File

@ -38,7 +38,7 @@ class UFMockRobot(Robot):
self.cameras = make_cameras_from_configs(config.cameras) self.cameras = make_cameras_from_configs(config.cameras)
self._gripper_type = 0 if not self.config.gripper_control else self.config.gripper_type if self.config.gripper_type > 0 else 1 self._gripper_type = self.config.gripper_type
@property @property
def _robot_state_features(self)-> dict: def _robot_state_features(self)-> dict:

View File

@ -21,8 +21,11 @@ class UFMockRobotConfig(RobotConfig):
robot_dof: int | None = None # Set it correctly if controlling in joint space! robot_dof: int | None = None # Set it correctly if controlling in joint space!
control_space: str = "joint" control_space: str = "joint"
gripper_control: bool = True
gripper_type: int = 1 # 1: xArm Gripper, 10: Pika Gripper gripper_type: int = 1 # 1: xArm Gripper, 10: Pika Gripper
observe_joint_vel: bool = False # only effective in joint control mode observe_joint_vel: bool = False # only effective in joint control mode
teleop: None = None # from lerobot.teleoperators import Teleoperator teleop: None = None # from lerobot.teleoperators import Teleoperator
state_offset_action: int = 3 # the number of previous teleop actions to be included in the observation state_offset_action: int = 3 # the number of previous teleop actions to be included in the observation
def __post_init__(self):
super().__post_init__()
self.id = 'uf_mock_robot' if self.id is None else self.id

View File

@ -9,3 +9,7 @@ class MultipleUFRobotConfig(RobotConfig):
async_connect: bool = True async_connect: bool = True
async_configure: bool = True async_configure: bool = True
async_action: bool = False async_action: bool = False
def __post_init__(self):
super().__post_init__()
self.id = 'multiple_uf_robot' if self.id is None else self.id

View File

@ -87,8 +87,14 @@ class UFRobot(Robot, Thread):
self._max_joint_velocity = math.radians(self.config.max_joint_velocity) self._max_joint_velocity = math.radians(self.config.max_joint_velocity)
self._max_linear_velocity = self.config.max_linear_velocity self._max_linear_velocity = self.config.max_linear_velocity
self._start_tcp_pose = self.config.start_tcp_pose if self.config.start_tcp_pose and len(self.config.start_tcp_pose) >= 6:
self._start_joints = self.config.start_joints self._start_tcp_pose = list(self.config.start_tcp_pose[:3]) + list(map(math.radians, self.config.start_tcp_pose[3:6]))
else:
self._start_tcp_pose = None
if self.config.start_joints:
self._start_joints = list(map(math.radians, self.config.start_joints))
else:
self._start_joints = None
self.report_stop_event = Event() self.report_stop_event = Event()
self._rt_report_normal = False self._rt_report_normal = False
@ -97,7 +103,7 @@ class UFRobot(Robot, Thread):
self._cart_obs_has_vel = any('velo.' in key for key in CARTESIAN_OBS_KEYS) self._cart_obs_has_vel = any('velo.' in key for key in CARTESIAN_OBS_KEYS)
self._jnt_obs_has_vel = self.config.observe_joint_vel self._jnt_obs_has_vel = self.config.observe_joint_vel
self._gripper_type = 0 if not self.config.gripper_control else self.config.gripper_type self._gripper_type = self.config.gripper_type
if self._gripper_type == GripperType.xArmGripper: if self._gripper_type == GripperType.xArmGripper:
gripper_speed = 5000 if self.config.gripper_speed < 0 else min(max(50, self.config.gripper_speed), 5000) gripper_speed = 5000 if self.config.gripper_speed < 0 else min(max(50, self.config.gripper_speed), 5000)
gripper_force = 50 if self.config.gripper_force < 0 else self.config.gripper_force # # not support gripper_force = 50 if self.config.gripper_force < 0 else self.config.gripper_force # # not support
@ -198,6 +204,8 @@ class UFRobot(Robot, Thread):
if calibrate: if calibrate:
self.calibrate() self.calibrate()
self.real_arm.set_linear_spd_limit_factor(2.0)
self._is_connected = True self._is_connected = True
def configure(self) -> None: def configure(self) -> None:
@ -206,22 +214,6 @@ class UFRobot(Robot, Thread):
self.real_arm.set_mode(0) # set to idle mode self.real_arm.set_mode(0) # set to idle mode
self.real_arm.set_state(0) # set to start state self.real_arm.set_state(0) # set to start state
time.sleep(0.5) time.sleep(0.5)
if self._start_tcp_pose is None:
self.real_arm.set_servo_angle(angle=self._start_joints, is_radian=True, wait=True)
else:
self.real_arm.set_servo_angle(angle=self._start_joints, is_radian=True, wait=True)
self.real_arm.set_position(*self._start_tcp_pose, speed=100, is_radian=True, wait=True)
_, self._start_joints = self.real_arm.get_servo_angle(is_radian=True)
self._start_tcp_pose = None
if self._control_space == "joint":
self.real_arm.set_mode(6)
elif self._control_space == "cartesian":
self.real_arm.set_mode(7)
else:
raise ValueError(f"Please check the given control space of uf_robot! got {self._control_space}")
self.real_arm.set_state(0)
_, err_warn = self.real_arm.get_err_warn_code() _, err_warn = self.real_arm.get_err_warn_code()
if err_warn[0] != 0: if err_warn[0] != 0:
@ -259,6 +251,26 @@ class UFRobot(Robot, Thread):
if err_warn[0] != 0: if err_warn[0] != 0:
raise RuntimeError(f"Failed to set correct state to Gripper! Controller Error code: {err_warn[0]} !") raise RuntimeError(f"Failed to set correct state to Gripper! Controller Error code: {err_warn[0]} !")
if self._start_joints is not None:
self.real_arm.set_servo_angle(angle=self._start_joints, is_radian=True, wait=True)
if self._start_tcp_pose is not None:
self.real_arm.set_position(*self._start_tcp_pose, speed=100, is_radian=True, wait=True)
_, self._start_joints = self.real_arm.get_servo_angle(is_radian=True)
self._start_tcp_pose = None
if self._control_space == "joint":
self.real_arm.set_mode(6)
elif self._control_space == "cartesian":
self.real_arm.set_mode(7)
else:
raise ValueError(f"Please check the given control space of uf_robot! got {self._control_space}")
self.real_arm.set_state(0)
_, err_warn = self.real_arm.get_err_warn_code()
if err_warn[0] != 0:
raise RuntimeError(f"Failed to set correct state to UF robot! Controller Error code: {err_warn[0]} !")
if self._use_rt_report and not self._rt_report_normal: if self._use_rt_report and not self._rt_report_normal:
self.start() self.start()
time.sleep(0.2) time.sleep(0.2)

View File

@ -1,6 +1,5 @@
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Tuple from typing import Tuple
import numpy as np
from lerobot.cameras import CameraConfig from lerobot.cameras import CameraConfig
from lerobot.cameras.realsense import RealSenseCameraConfig from lerobot.cameras.realsense import RealSenseCameraConfig
from lerobot.robots import RobotConfig from lerobot.robots import RobotConfig
@ -30,15 +29,17 @@ class UFRobotConfig(RobotConfig):
robot_ip: str = "192.168.1.127" robot_ip: str = "192.168.1.127"
robot_dof: int | None = None # Set it correctly if controlling in joint space! robot_dof: int | None = None # Set it correctly if controlling in joint space!
control_space: str = "joint" control_space: str = "joint"
gripper_control: bool = True
gripper_type: int = 1 # 1: xArm Gripper, 2: xArm Gripper G2, 10: Pika Gripper, 11: Robotiq 2F-85 gripper_type: int = 1 # 1: xArm Gripper, 2: xArm Gripper G2, 10: Pika Gripper, 11: Robotiq 2F-85
gripper_port: str = None # only used by pika gripper (gripper_type=10) gripper_port: str = None # only used by pika gripper (gripper_type=10)
gripper_speed: int = -1 # auto gripper_speed: int = -1 # auto
gripper_force: int = -1 # auto gripper_force: int = -1 # auto
observe_joint_vel: bool = False # only effective in joint control mode observe_joint_vel: bool = False # only effective in joint control mode
start_joints: Tuple[float, ...] = (0, 0, 0, np.pi/2, 0, np.pi/2, 0) start_joints: Tuple[float, ...] = (0, 0, 0, 90, 0, 90, 0) # °
start_tcp_pose: Tuple[float, ...] = None # xyzrpy start_tcp_pose: Tuple[float, ...] = None # [x, y, z, roll(°), pitch(°), yaw(°)]
max_joint_velocity: int = 90 # °/s, only effective in joint control mode max_joint_velocity: int = 90 # °/s, only effective in joint control mode
max_linear_velocity: int = 200 # mm/s, only effective in cartesian control mode max_linear_velocity: int = 200 # mm/s, only effective in cartesian control mode
rx_continuous: bool = False no_action: bool = False # only for debug
no_action: bool = False
def __post_init__(self):
super().__post_init__()
self.id = 'uf_robot' if self.id is None else self.id

View File

@ -27,7 +27,6 @@ from lerobot.robots import ( # noqa: F401
) )
from lerobot.utils.control_utils import ( from lerobot.utils.control_utils import (
is_headless, is_headless,
init_keyboard_listener,
predict_action, predict_action,
) )
from lerobot.utils.import_utils import register_third_party_plugins from lerobot.utils.import_utils import register_third_party_plugins
@ -40,7 +39,7 @@ from lerobot.configs import parser
from lerobot.configs.policies import PreTrainedConfig from lerobot.configs.policies import PreTrainedConfig
from lerobot.scripts.lerobot_record import DatasetRecordConfig from lerobot.scripts.lerobot_record import DatasetRecordConfig
from lerobot.datasets.lerobot_dataset import LeRobotDataset from lerobot.datasets.lerobot_dataset import LeRobotDataset
from ufactory_lerobot.utils.utils import instantiate_from_dict from ufactory_lerobot.utils.utils import instantiate_from_dict, init_keyboard_listener
from ufactory_lerobot.devices.umi.vive_tracker.transformations import Transformations from ufactory_lerobot.devices.umi.vive_tracker.transformations import Transformations
@ -74,8 +73,8 @@ def compute_relative_axis_angle(rot_prev, rot_curr):
返回: 相对轴角向量 返回: 相对轴角向量
""" """
# 1. 转为矩阵 # 1. 转为矩阵
R_prev = Transformations.rxryrz_to_matrix(rot_prev) R_prev = Transformations.rxryrz_to_rotation_matrix(rot_prev)
R_curr = Transformations.rxryrz_to_matrix(rot_curr) R_curr = Transformations.rxryrz_to_rotation_matrix(rot_curr)
# 2. 计算相对旋转矩阵 # 2. 计算相对旋转矩阵
# R_delta 表示从 prev 坐标系到 curr 坐标系的旋转 # R_delta 表示从 prev 坐标系到 curr 坐标系的旋转
@ -88,8 +87,8 @@ def compute_target_axis_angle(rot_prev, rot_delta):
""" """
根据起始轴角和相对轴角计算目标轴角 根据起始轴角和相对轴角计算目标轴角
""" """
R_prev = Transformations.rxryrz_to_matrix(rot_prev) R_prev = Transformations.rxryrz_to_rotation_matrix(rot_prev)
R_delta = Transformations.rxryrz_to_matrix(rot_delta) R_delta = Transformations.rxryrz_to_rotation_matrix(rot_delta)
R_curr = R_prev @ R_delta R_curr = R_prev @ R_delta
# R_curr = R_prev.apply(R_delta) # R_curr = R_prev.apply(R_delta)
return Transformations.rotation_matrix_to_rxryrz(R_curr) return Transformations.rotation_matrix_to_rxryrz(R_curr)
@ -122,7 +121,7 @@ class EvalConfig:
return ["policy"] return ["policy"]
def eval_loop(cfg: EvalConfig, relative=False): def eval_loop(cfg: EvalConfig, relative=False, rx_continuous=False):
init_logging() init_logging()
logging.info(pformat(asdict(cfg))) logging.info(pformat(asdict(cfg)))
@ -238,8 +237,6 @@ def eval_loop(cfg: EvalConfig, relative=False):
print("\n********** Policy Eval Episode Loop Start **********") print("\n********** Policy Eval Episode Loop Start **********")
print(f'relative: {relative}') print(f'relative: {relative}')
rx_continuous = getattr(cfg.robot, 'rx_continuous', False)
# with torch.no_grad(), torch.autocast(device_type=device.type) if cfg.policy.use_amp else nullcontext(): # with torch.no_grad(), torch.autocast(device_type=device.type) if cfg.policy.use_amp else nullcontext():
while True: while True:
robot.configure() robot.configure()
@ -278,8 +275,7 @@ def eval_loop(cfg: EvalConfig, relative=False):
# Get robot observation # Get robot observation
obs = robot.get_observation() obs = robot.get_observation()
# if rx_continuous and not relative and 'pose.rx' in obs and obs['pose.rx'] < 0:
# obs['pose.rx'] += 2 * math.pi
curr_robot_dict = {} curr_robot_dict = {}
curr_action_dict = {} curr_action_dict = {}
for key in keys: for key in keys:
@ -425,6 +421,7 @@ def main():
parser.add_argument('--policy.path', type=str, required=True, parser.add_argument('--policy.path', type=str, required=True,
help='configuration file path, e.g.my_config.yaml') help='configuration file path, e.g.my_config.yaml')
parser.add_argument('--relative', action='store_true', help='is relative motion or not') parser.add_argument('--relative', action='store_true', help='is relative motion or not')
parser.add_argument('--rx_continuous', action='store_true', help='rx continuous or not')
args = parser.parse_args() args = parser.parse_args()
try: try:
with open(args.config, 'r') as f: with open(args.config, 'r') as f:
@ -436,7 +433,7 @@ def main():
config = instantiate_from_dict(cfg) config = instantiate_from_dict(cfg)
eval_cfg = EvalConfig(robot=config["RobotConfig"], dataset=config["DatasetRecordConfig"]) eval_cfg = EvalConfig(robot=config["RobotConfig"], dataset=config["DatasetRecordConfig"])
eval_loop(eval_cfg, args.relative) eval_loop(eval_cfg, args.relative, args.rx_continuous)
if __name__ == "__main__": if __name__ == "__main__":

View File

@ -1,11 +1,12 @@
import yaml import yaml
import time
import argparse import argparse
from pathlib import Path from pathlib import Path
import ufactory_lerobot # patch import ufactory_lerobot # patch
from lerobot.scripts.lerobot_record import * from lerobot.scripts.lerobot_record import *
from ufactory_lerobot.teleoperators.uf_mock_teleop import UFMockTeleop from ufactory_lerobot.teleoperators.uf_mock_teleop import UFMockTeleop
from ufactory_lerobot.teleoperators.base_teleop import UFBaseTeleop from ufactory_lerobot.teleoperators.base_teleop import UFBaseTeleop
from ufactory_lerobot.utils.utils import instantiate_from_dict from ufactory_lerobot.utils.utils import instantiate_from_dict, init_keyboard_listener
@safe_stop_image_writer @safe_stop_image_writer
@ -239,31 +240,71 @@ def record(cfg: RecordConfig) -> LeRobotDataset:
if teleop is not None: if teleop is not None:
teleop.connect() teleop.connect()
listener, events = init_keyboard_listener() is_evt = not is_headless()
is_uf_teleop = isinstance(teleop, UFBaseTeleop)
is_recorded = False
key_dict = {}
events = {"exit_early": False, "rerecord_episode": False, "stop_recording": False}
print("\n********** Episode Record Loop Start **********") if is_evt:
from pynput import keyboard
if isinstance(teleop, UFBaseTeleop): key_dict = {
# if getattr(cfg.robot, 'rx_continuous', False): keyboard.Key.space: 0, # start
# def frame_callback(frame): keyboard.Key.enter: 0, # help
# if frame['action'][3] < 0: }
# frame['action'][3] += 2 * math.pi
# if frame['observation.state'][3] < 0: def on_press(key):
# frame['observation.state'][3] += 2 * math.pi try:
# return frame if key == keyboard.Key.right:
# else: print("Right arrow key pressed. Exiting loop...")
# frame_callback = None events["exit_early"] = True
frame_callback = None elif key == keyboard.Key.left:
input('\nPress Enter to record this episode >>>>> ') print("Left arrow key pressed. Exiting loop and rerecord the last episode...")
time.sleep(0.5) events["rerecord_episode"] = True
teleop.set_ctrl_status(True) events["exit_early"] = True
time.sleep(0.5) elif key == keyboard.Key.esc:
print("Escape key pressed. Stopping data recording...")
events["stop_recording"] = True
events["exit_early"] = True
except Exception as e:
print(f"Error handling key press: {e}")
if key in key_dict:
key_dict[key] = True
def on_release(key):
try:
if key == keyboard.Key.enter:
if not is_recorded:
print('[HELP] <ESC>: EXIT, <SPACE>: START, <LEFT ARROW>: RESET, <RIGH ARROW>: SAVE')
else:
print('[HELP] <ESC>: EXIT, <LEFT ARROW>: RESET, <RIGH ARROW>: SAVE')
# is_recorded = True
except Exception as e:
print(f"Error handling key release: {e}")
if key in key_dict:
key_dict[key] = False
listener, events = init_keyboard_listener(events=events, on_press=on_press, on_release=on_release)
print("\n********** Episode Record Loop Start **********")
print('[HELP] <ESC>: EXIT, <SPACE>: START, <LEFT ARROW>: RESET, <RIGH ARROW>: SAVE')
else: else:
frame_callback = None input('[HELP] Enter to to start record >>> ')
if is_uf_teleop:
teleop.set_teleop_enabled(True)
is_recorded = True
print("\n********** Episode Record Loop Start **********")
frame_callback = None
with VideoEncodingManager(dataset): with VideoEncodingManager(dataset):
recorded_episodes = 0 recorded_episodes = 0
while recorded_episodes < cfg.dataset.num_episodes and not events["stop_recording"]: while recorded_episodes < cfg.dataset.num_episodes and not events["stop_recording"]:
time.sleep(0.01)
if is_evt:
if not is_recorded and key_dict[keyboard.Key.space]:
is_recorded = True
if teleop is not None and isinstance(teleop, UFMockTeleop): if teleop is not None and isinstance(teleop, UFMockTeleop):
if events["stop_recording"]: if events["stop_recording"]:
continue continue
@ -275,52 +316,65 @@ def record(cfg: RecordConfig) -> LeRobotDataset:
continue continue
if events["stop_recording"]: if events["stop_recording"]:
continue continue
is_recorded = True
log_say(f"Recording episode {dataset.num_episodes}", cfg.play_sounds) if is_recorded:
record_loop( events["rerecord_episode"] = False
robot=robot, events["exit_early"] = False
events=events, if is_uf_teleop:
fps=cfg.dataset.fps, robot.configure()
teleop_action_processor=teleop_action_processor, obs = robot.get_observation()
robot_action_processor=robot_action_processor, teleop.set_teleop_enabled(True, obs)
robot_observation_processor=robot_observation_processor, log_say(f"Recording episode {dataset.num_episodes}", cfg.play_sounds)
teleop=teleop, record_loop(
policy=policy, robot=robot,
preprocessor=preprocessor, events=events,
postprocessor=postprocessor, fps=cfg.dataset.fps,
dataset=dataset, teleop_action_processor=teleop_action_processor,
control_time_s=cfg.dataset.episode_time_s, robot_action_processor=robot_action_processor,
single_task=cfg.dataset.single_task, robot_observation_processor=robot_observation_processor,
display_data=cfg.display_data, teleop=teleop,
frame_callback=frame_callback, policy=policy,
) preprocessor=preprocessor,
postprocessor=postprocessor,
dataset=dataset,
control_time_s=cfg.dataset.episode_time_s,
single_task=cfg.dataset.single_task,
display_data=cfg.display_data,
frame_callback=frame_callback,
)
else:
continue
if events['stop_recording']:
break
if events["rerecord_episode"]: if events["rerecord_episode"]:
log_say("Re-record episode", cfg.play_sounds) log_say("Re-record episode", cfg.play_sounds)
events["rerecord_episode"] = False events["rerecord_episode"] = False
events["exit_early"] = False events["exit_early"] = False
if isinstance(teleop, UFBaseTeleop): if is_uf_teleop:
teleop.set_ctrl_status(False) teleop.set_teleop_enabled(False)
dataset.clear_episode_buffer() if dataset.episode_buffer:
input('\nPress Enter to rerecord this episode >>>>> ') dataset.clear_episode_buffer()
is_recorded = False
if isinstance(teleop, UFBaseTeleop): if is_evt:
robot.configure() print('[HELP] <ESC>: EXIT, <SPACE>: START, <LEFT ARROW>: RESET, <RIGH ARROW>: SAVE')
time.sleep(0.5) else:
teleop.set_ctrl_status(True) input('\nPress Enter to rerecord this episode >>>>> ')
time.sleep(0.5) is_recorded = True
continue continue
if not events['stop_recording']: if is_recorded and not events['stop_recording']:
if isinstance(teleop, UFBaseTeleop): log_say(f"Save episode {dataset.num_episodes}", cfg.play_sounds)
teleop.set_ctrl_status(False) if is_uf_teleop:
teleop.set_teleop_enabled(False)
dataset.save_episode() dataset.save_episode()
recorded_episodes += 1 recorded_episodes += 1
input('Press Enter to record at the next episode >>>>> ') is_recorded = False
if isinstance(teleop, UFBaseTeleop): if is_evt:
robot.configure() print('[HELP] <ESC>: EXIT, <SPACE>: START, <LEFT ARROW>: RESET, <RIGH ARROW>: SAVE')
time.sleep(1) else:
teleop.set_ctrl_status(True) input('Press Enter to record at the next episode >>>>> ')
is_recorded = True
print("\n********** Episode Record Loop Exit **********") print("\n********** Episode Record Loop Exit **********")
@ -328,7 +382,7 @@ def record(cfg: RecordConfig) -> LeRobotDataset:
if teleop is not None: if teleop is not None:
teleop.disconnect() teleop.disconnect()
if not is_headless() and listener is not None: if is_evt and listener is not None:
listener.stop() listener.stop()
if cfg.dataset.push_to_hub: if cfg.dataset.push_to_hub:

View File

@ -18,16 +18,14 @@ from lerobot.teleoperators import ( # noqa: F401
TeleoperatorConfig, TeleoperatorConfig,
make_teleoperator_from_config, make_teleoperator_from_config,
) )
from lerobot.utils.control_utils import (
is_headless,
init_keyboard_listener
)
from lerobot.utils.import_utils import register_third_party_plugins from lerobot.utils.import_utils import register_third_party_plugins
from lerobot.utils.robot_utils import precise_sleep from lerobot.utils.robot_utils import precise_sleep
from lerobot.utils.utils import ( from lerobot.utils.utils import (
init_logging, init_logging,
) )
from ufactory_lerobot.utils.utils import instantiate_from_dict from ufactory_lerobot.utils.utils import instantiate_from_dict, is_headless, init_keyboard_listener
from ufactory_lerobot.teleoperators.base_teleop import UFBaseTeleop
@dataclass @dataclass
class TeleopConfig: class TeleopConfig:
@ -50,32 +48,107 @@ def teleop_loop(cfg: TeleopConfig):
robot.connect() robot.connect()
teleop.connect() teleop.connect()
events = {"exit": False}
listener = None
if not is_headless():
from pynput import keyboard
def on_press(key):
try:
if key == keyboard.Key.esc:
print("Escape key pressed. Stopping ...")
events["exit"] = True
except Exception as e:
print(f"Error handling key press: {e}")
listener, events = init_keyboard_listener(events=events, on_press=on_press)
sleep_time_s = 1 / cfg.fps sleep_time_s = 1 / cfg.fps
print("\n********** Test Teleop With Robot **********") is_evt = not is_headless()
input('Enter to control robot with teleop >>> ') is_uf_teleop = isinstance(teleop, UFBaseTeleop)
print("\n********** Teleop Control Loop Start **********") is_reset = False
is_paused = True
events = {"exit": False}
listener = None
key_dict = {}
if is_evt:
from pynput import keyboard
key_dict = {
keyboard.Key.esc: 0, # exit
keyboard.Key.left: 0, # reset and pause
keyboard.Key.space: 0, # start/pause
keyboard.Key.enter: 0, # help
}
def on_press(key):
if key_dict.get(key, 1) == 0:
try:
if key == keyboard.Key.esc:
events["exit"] = True
print("\nEscape key pressed. Stopping ...")
except Exception as e:
print(f"Error handling key press: {e}")
if key in key_dict:
key_dict[key] = True
def on_release(key):
try:
if key == keyboard.Key.enter:
if is_paused:
if is_reset:
print('[HELP] <ESC>: EXIT, <SPACE>: RESET AND START, <LEFT ARROW>: RESET')
else:
print('[HELP] <ESC>: EXIT, <SPACE>: START, <LEFT ARROW>: RESET')
else:
print('[HELP] <ESC>: EXIT, <SPACE>: PAUSE, <LEFT ARROW>: RESET')
except Exception as e:
print(f"Error handling key release: {e}")
if key in key_dict:
key_dict[key] = False
listener, events = init_keyboard_listener(events=events, on_press=on_press, on_release=on_release)
print("\n********** Teleop Control Loop Start **********")
print('[HELP] <ESC>: EXIT, <SPACE>: START, <LEFT ARROW>: RESET')
else:
input('[HELP] Enter to control robot with teleop >>> ')
if is_uf_teleop:
obs = robot.get_observation()
teleop.set_teleop_enabled(True, obs)
is_paused = False
is_reset = False
print("\n********** Teleop Control Loop Start **********")
key_space_pressed = False
key_left_pressed = False
while not events["exit"]: while not events["exit"]:
start_loop_t = time.perf_counter() start_loop_t = time.perf_counter()
if is_evt:
if key_dict[keyboard.Key.left] and not key_left_pressed:
key_left_pressed = True
is_reset = True
if not is_paused:
is_paused = True
if is_uf_teleop:
teleop.set_teleop_enabled(False)
print('[HELP] <ESC>: EXIT, <SPACE>: RESET AND START, <LEFT ARROW>: RESET')
elif not key_dict[keyboard.Key.left] and key_left_pressed:
key_left_pressed = False
if key_dict[keyboard.Key.space] and not key_space_pressed:
key_space_pressed = True
is_paused = not is_paused
if is_paused:
if is_uf_teleop:
teleop.set_teleop_enabled(False)
# print('========== Teleop is paused ==========')
print('[HELP] <ESC>: EXIT, <SPACE>: START, <LEFT ARROW>: RESET')
else:
if is_reset:
is_reset = False
robot.configure()
# print('========== Teleop is start ==========')
if is_uf_teleop:
obs = robot.get_observation()
teleop.set_teleop_enabled(True, obs)
print('[HELP] <ESC>: EXIT, <SPACE>: PAUSE, <LEFT ARROW>: RESET')
continue
elif not key_dict[keyboard.Key.space] and key_space_pressed:
key_space_pressed = False
if is_reset or is_paused:
continue
# Get robot observation # Get robot observation
obs = robot.get_observation() obs = robot.get_observation()
@ -91,7 +164,7 @@ def teleop_loop(cfg: TeleopConfig):
print("\n********** Teleop Control Loop Exit **********") print("\n********** Teleop Control Loop Exit **********")
robot.disconnect() robot.disconnect()
teleop.disconnect() teleop.disconnect()
if not is_headless() and listener is not None: if is_evt and listener is not None:
listener.stop() listener.stop()
def main(): def main():

View File

@ -9,5 +9,9 @@ class UFBaseTeleop(Teleoperator):
super().__init__(config) super().__init__(config)
self.config = config self.config = config
def set_ctrl_status(self, status): def set_teleop_enabled(self, enabled: bool, obs=None):
"""
启用/停用遥操作
当enabled为True且obs不为None时, 顺便设置机械臂初始位置映射
"""
pass pass

View File

@ -1,6 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
import logging import logging
import time import time
import math
import numpy as np import numpy as np
from gello.dynamixel.driver import DynamixelDriver from gello.dynamixel.driver import DynamixelDriver
from gello.agents.gello_agent import GelloAgent, DynamixelRobotConfig from gello.agents.gello_agent import GelloAgent, DynamixelRobotConfig
@ -36,8 +37,9 @@ class GelloTeleop(UFBaseTeleop):
curr_joints = driver.get_joints() curr_joints = driver.get_joints()
driver.close() driver.close()
joint_offsets = [] joint_offsets = []
for i in range(len(self.config.start_joints)): start_joints = list(map(math.radians, self.config.start_joints))
offset = curr_joints[i] - self.config.start_joints[i] / self.config.joint_signs[i] for i in range(len(start_joints)):
offset = curr_joints[i] - start_joints[i] / self.config.joint_signs[i]
joint_offsets.append(offset) joint_offsets.append(offset)
if self.config.gripper_id >= 0: if self.config.gripper_id >= 0:
gripper_config = [self.config.gripper_id, np.rad2deg(curr_joints[-1]) - 0.2, np.rad2deg(curr_joints[-1]) - 42] gripper_config = [self.config.gripper_id, np.rad2deg(curr_joints[-1]) - 0.2, np.rad2deg(curr_joints[-1]) - 42]
@ -52,7 +54,7 @@ class GelloTeleop(UFBaseTeleop):
} }
self._dynamixel_robo_config = DynamixelRobotConfig(**param_dict) self._dynamixel_robo_config = DynamixelRobotConfig(**param_dict)
print(self._dynamixel_robo_config) print(self._dynamixel_robo_config)
self.dof = len(self.config.start_joints) self.dof = len(start_joints)
if self.config.torque_joint_ids: if self.config.torque_joint_ids:
driver = DynamixelDriver(self.config.torque_joint_ids, port=self.config.port, baudrate=57600) driver = DynamixelDriver(self.config.torque_joint_ids, port=self.config.port, baudrate=57600)

View File

@ -1,6 +1,5 @@
#!/usr/bin/env python #!/usr/bin/env python
import numpy as np
from dataclasses import dataclass from dataclasses import dataclass
from typing import Tuple from typing import Tuple
from lerobot.teleoperators import TeleoperatorConfig from lerobot.teleoperators import TeleoperatorConfig
@ -15,6 +14,9 @@ class GelloTeleopConfig(TeleoperatorConfig):
# Others: Calibration angles, joint directions etc # Others: Calibration angles, joint directions etc
joint_ids: Tuple[int, ...] = (1, 2, 3, 4, 5, 6, 7) joint_ids: Tuple[int, ...] = (1, 2, 3, 4, 5, 6, 7)
joint_signs: Tuple[int, ...] = (1, 1, 1, 1, 1, 1, 1) # if follow the original open-sourced gello xarm7 setup joint_signs: Tuple[int, ...] = (1, 1, 1, 1, 1, 1, 1) # if follow the original open-sourced gello xarm7 setup
start_joints: Tuple[float, ...] = (0, 0, 0, np.pi/2, 0, np.pi/2, 0) start_joints: Tuple[float, ...] = (0, 0, 0, 90, 0, 90, 0) # °
gripper_id: int = 8 # -1: no gripper gripper_id: int = 8 # -1: no gripper
torque_joint_ids: Tuple[int, ...] = None # the joints will activate torque mode. torque_joint_ids: Tuple[int, ...] = None # the joints will activate torque mode.
def __post_init__(self):
self.id = 'gello_teleop' if self.id is None else self.id

View File

@ -2,11 +2,9 @@
import time import time
import math import math
import numpy as np
from typing import Any from typing import Any
from threading import Thread, Event, Lock from threading import Thread, Event, Lock
from lerobot.utils.errors import DeviceAlreadyConnectedError, DeviceNotConnectedError from lerobot.utils.errors import DeviceNotConnectedError
from xarm.wrapper import XArmAPI
from ufactory_lerobot.devices.pika import PikaDevice from ufactory_lerobot.devices.pika import PikaDevice
from ufactory_lerobot.devices.umi.vive_tracker.transformations import Transformations from ufactory_lerobot.devices.umi.vive_tracker.transformations import Transformations
from ..base_teleop import UFBaseTeleop from ..base_teleop import UFBaseTeleop
@ -18,7 +16,7 @@ class PikaTeleop(UFBaseTeleop, Thread):
config_class = PikaTeleopConfig config_class = PikaTeleopConfig
name = "Pika Teleop For xArm" name = "Pika Teleop For xArm"
def __init__(self, config: PikaTeleopConfig): def __init__(self, config: PikaTeleopConfig, prefix=''):
super().__init__(config) super().__init__(config)
Thread.__init__(self) # Do NOT REMOVE! Thread.__init__(self) # Do NOT REMOVE!
@ -27,20 +25,22 @@ class PikaTeleop(UFBaseTeleop, Thread):
self._is_connected = False self._is_connected = False
self._is_calibrated = True self._is_calibrated = True
self._data_lock = Lock() self._data_lock = Lock()
self._ctrl_flag = False self._teleop_enabled = False
self._last_action = None
self._need_initial = False self._need_initial = False
self.prefix = '' if not prefix else f'{prefix}.'
tracker_to_robot_eef = list(self.config.tracker_to_robot_eef[:3]) + list(map(math.radians, self.config.tracker_to_robot_eef[3:6]))
self.tracker_to_robot_matrix = Transformations.xyzrpy_to_rotation_matrix(*tracker_to_robot_eef)
robot_base_pose = list(self.config.robot_base_pose[:3]) + list(map(math.radians, self.config.robot_base_pose[3:6]))
self.robot_base_matrix = Transformations.xyzrpy_to_rotation_matrix(*robot_base_pose)
self.begin_tracker_robot_matrix = None
self._last_robot_pose = Transformations.rotation_matrix_to_xyzrxryrz(self.robot_base_matrix)
self._last_gripper_pos = 0.0
self.pika_device = PikaDevice(1, pika_sense_port=self.config.port) self.pika_device = PikaDevice(1, pika_sense_port=self.config.port)
self.pika_sense = self.pika_device.pika_sense self.pika_sense = self.pika_device.pika_sense
if self.config.robot_ip:
self.arm = XArmAPI(self.config.robot_ip, is_radian=True)
else:
self.arm = None
self._robot_target_pose = None
self._gripper_target_pos = None
@property @property
def action_features(self) -> dict: def action_features(self) -> dict:
if self.config.use_gripper: if self.config.use_gripper:
@ -97,127 +97,45 @@ class PikaTeleop(UFBaseTeleop, Thread):
self._is_connected = False self._is_connected = False
self.join() self.join()
def set_ctrl_status(self, status): def set_teleop_enabled(self, enabled: bool, obs=None):
if status: with self._data_lock:
if not self._ctrl_flag: if enabled:
print('开始遥操作') if obs is not None:
self._ctrl_flag = True self._last_robot_pose = [obs[f"{self.prefix}pose.x"], obs[f"{self.prefix}pose.y"], obs[f"{self.prefix}pose.z"], obs[f"{self.prefix}pose.rx"], obs[f"{self.prefix}pose.ry"], obs[f"{self.prefix}pose.rz"]]
self._need_initial = True if self.config.use_gripper:
else: self._last_gripper_pos = obs[f"{self.prefix}gripper.pos"]
self._ctrl_flag = False self.robot_base_matrix = Transformations.xyzrxryrz_to_rotation_matrix(*self._last_robot_pose)
self._need_initial = False self.begin_tracker_robot_matrix = None
print('停止遥操作') self._last_action = None
self._teleop_enabled = True
print(f'[{self.prefix}PIKA] Teleoperation is start')
else:
obs = self._last_action
self._last_robot_pose = [obs[f"{self.prefix}pose.x"], obs[f"{self.prefix}pose.y"], obs[f"{self.prefix}pose.z"], obs[f"{self.prefix}pose.rx"], obs[f"{self.prefix}pose.ry"], obs[f"{self.prefix}pose.rz"]]
if self.config.use_gripper:
self._last_gripper_pos = obs[f"{self.prefix}gripper.pos"]
self._teleop_enabled = False
self._last_action = None
print(f'[{self.prefix}PIKA] Teleoperation has paused')
def run(self): def run(self):
self._is_connected = True self._is_connected = True
init_state = self.pika_sense.get_command_state() init_state = self.pika_sense.get_command_state()
curr_state = init_state curr_state = init_state
last_gripper_distance = 0
self._ctrl_flag = False # 是否开启遥操作
self._need_initial = False
sleep_time = 1 / self.config.frequency sleep_time = 1 / self.config.frequency
if self.arm:
self.arm.set_linear_spd_limit_factor(2.0)
pika_to_robot_eef = [0, 0, 0, math.pi, -math.pi / 2, 0] # rpy
# pika_to_robot_eef = [0, 0, 0, math.pi, 0, 0]
# pika坐标系到机械臂坐标系的变换关系对应的变换矩阵
pika_to_robot_matrix = Transformations.xyzrpy_to_rotation_matrix(*pika_to_robot_eef)
# 机械臂初始位置对应的变换矩阵
# robot_base_matrix = Transformations.xyzrpy_to_rotation_matrix(*[0, 0, 190, -np.pi, -np.radians(41), 0])
robot_base_matrix = Transformations.xyzrpy_to_rotation_matrix(*[300, 0, 365, np.pi, 0, 0])
# pika初始位置转换到机械臂坐标系后对应的变换矩阵
pika_begin_robot_matrix = None
# pika目标位置转换到机械臂坐标系后对应的变换矩阵
pika_end_robot_matrix = None
scale_xyz = self.config.scale_xyz
while not self.stop_event.is_set(): while not self.stop_event.is_set():
time.sleep(sleep_time) time.sleep(sleep_time)
if not self.arm and pika_begin_robot_matrix is None:
pose = self.pika_sense.get_pose(self.pika_device.pika_tracker_device)
if not pose:
continue
x, y, z = pose.position[0] * 1000 * scale_xyz, pose.position[1] * 1000 * scale_xyz, pose.position[2] * 1000 * scale_xyz
pika_begin_robot_matrix = Transformations.tracker_pose_to_robot_matrix(x, y, z, pose.rotation, pika_to_robot_matrix)
robot_target_pose = Transformations.tracker_robot_matrix_to_robot_pose(pika_begin_robot_matrix, pika_begin_robot_matrix, robot_base_matrix, is_axis_angle=True)
print('初始绑定, 当前Pika位置对应的机械臂目标位置: x={:.6f}, y={:.6f}, z={:.6f}, rx={:.6f}, ry={:.6f}, rz={:.6f}'.format(robot_target_pose[0], robot_target_pose[1], robot_target_pose[2], math.degrees(robot_target_pose[3]), math.degrees(robot_target_pose[4]), math.degrees(robot_target_pose[5])))
continue
state = self.pika_sense.get_command_state() state = self.pika_sense.get_command_state()
if state != curr_state: if state != curr_state:
curr_state = state curr_state = state
if not self._ctrl_flag and curr_state != init_state: if not self._teleop_enabled and curr_state != init_state:
self._ctrl_flag = True self.set_teleop_enabled(True, self._last_action)
self._need_initial = True
# self.robot_init()
print('开始遥操作')
time.sleep(1) time.sleep(1)
elif self._ctrl_flag and curr_state == init_state: elif self._teleop_enabled and curr_state == init_state:
self._ctrl_flag = False self.self.set_teleop_enabled(False)
print('停止遥操作')
continue continue
if self._ctrl_flag and self.arm and (not self.arm.connected or self.arm.error_code != 0 or self.arm.state >= 4):
print('机械臂原因, 遥操作自动停止')
init_state = state
curr_state = state
self._ctrl_flag = False
continue
if not self._ctrl_flag:
continue
if self.config.use_gripper:
distance = min(max(self.pika_sense.get_gripper_distance(), 0), 100)
if abs(last_gripper_distance - distance) > 2:
last_gripper_distance = distance
with self._data_lock:
self._gripper_target_pos = last_gripper_distance
pose = self.pika_sense.get_pose(self.pika_device.pika_tracker_device)
if not pose:
continue
x, y, z = pose.position[0] * 1000 * scale_xyz, pose.position[1] * 1000 * scale_xyz, pose.position[2] * 1000 * scale_xyz
if not self.arm:
# 只有PIKA设备, 没有机械臂
pika_end_robot_matrix = Transformations.tracker_pose_to_robot_matrix(x, y, z, pose.rotation, pika_to_robot_matrix)
robot_target_pose = Transformations.tracker_robot_matrix_to_robot_pose(pika_begin_robot_matrix, pika_end_robot_matrix, robot_base_matrix, is_axis_angle=True)
if self._need_initial:
self._need_initial = False
print('[初始位置] x={:.6f}, y={:.6f}, z={:.6f}, rx={:.6f}, ry={:.6f}, rz={:.6f}'.format(robot_target_pose[0], robot_target_pose[1], robot_target_pose[2], math.degrees(robot_target_pose[3]), math.degrees(robot_target_pose[4]), math.degrees(robot_target_pose[5])))
else:
if self._need_initial:
self._need_initial = False
# _, robot_pos = self.arm.get_position()
_, robot_pos = self.arm.get_position(is_radian=True)
robot_base_pose = robot_pos
print('[初始] 机械臂位置: {}'.format(robot_pos))
# 机械臂初始位置对应的变换矩阵
robot_base_matrix = Transformations.xyzrpy_to_rotation_matrix(*robot_pos)
# pika初始位置转换到机械臂坐标系后对应的变换矩阵
pika_begin_robot_matrix = Transformations.tracker_pose_to_robot_matrix(x, y, z, pose.rotation, pika_to_robot_matrix)
pika_end_robot_matrix = pika_begin_robot_matrix
else:
# pika目标位置转换到机械臂坐标系后对应的变换矩阵
pika_end_robot_matrix = Transformations.tracker_pose_to_robot_matrix(x, y, z, pose.rotation, pika_to_robot_matrix)
robot_target_pose = Transformations.tracker_robot_matrix_to_robot_pose(pika_begin_robot_matrix, pika_end_robot_matrix, robot_base_matrix, is_axis_angle=True)
with self._data_lock:
self._robot_target_pose = robot_target_pose
# delta action # delta action
def get_action(self) -> dict[str, Any]: def get_action(self) -> dict[str, Any]:
@ -225,39 +143,47 @@ class PikaTeleop(UFBaseTeleop, Thread):
raise DeviceNotConnectedError( raise DeviceNotConnectedError(
"PikaTeleop is not connected. You need to run `connect()` before `get_action()`." "PikaTeleop is not connected. You need to run `connect()` before `get_action()`."
) )
with self._data_lock: with self._data_lock:
if self._robot_target_pose is not None: if self._last_action is None:
robot_target_pose = self._robot_target_pose.copy() self._last_action = {
else: f"{self.prefix}pose.x": self._last_robot_pose[0],
robot_target_pose = None f"{self.prefix}pose.y": self._last_robot_pose[1],
if self._gripper_target_pos is not None: f"{self.prefix}pose.z": self._last_robot_pose[2],
gripper_target_pos = (100 - self._gripper_target_pos) / (100 - 0) f"{self.prefix}pose.rx": self._last_robot_pose[3],
else: f"{self.prefix}pose.ry": self._last_robot_pose[4],
gripper_target_pos = 0.0 f"{self.prefix}pose.rz": self._last_robot_pose[5],
}
if self.config.use_gripper:
self._last_action.update({f"{self.prefix}gripper.pos": self._last_gripper_pos})
if not self._teleop_enabled:
return self._last_action
if robot_target_pose is None: pose = self.pika_sense.get_pose(self.pika_device.pika_tracker_device)
if self.arm: if pose:
_, robot_target_pose = self.arm.get_position_aa(is_radian=True) x, y, z = pose.position[0] * 1000 * self.config.scale_xyz, pose.position[1] * 1000 * self.config.scale_xyz, pose.position[2] * 1000 * self.config.scale_xyz
else: quaternion = pose.rotation
# robot_target_pose = [0, 0, 190, -np.pi, -np.radians(41), 0] tracker_robot_matrix = Transformations.tracker_pose_to_robot_matrix(x, y, z, quaternion, self.tracker_to_robot_matrix)
robot_target_pose = [300, 0, 365, np.pi, 0, 0] if self.begin_tracker_robot_matrix is None:
# print(self._robot_target_pose, robot_target_pose) self.begin_tracker_robot_matrix = tracker_robot_matrix
robot_target_pose = Transformations.tracker_robot_matrix_to_robot_pose(self.begin_tracker_robot_matrix, tracker_robot_matrix, self.robot_base_matrix, is_axis_angle=True)
# output is delta change of the robot pose # print(['{:.6f}'.format(val) for val in robot_target_pose])
action_dict = { self._last_action[f"{self.prefix}pose.x"] = robot_target_pose[0]
"pose.x": robot_target_pose[0], self._last_action[f"{self.prefix}pose.y"] = robot_target_pose[1]
"pose.y": robot_target_pose[1], self._last_action[f"{self.prefix}pose.z"] = robot_target_pose[2]
"pose.z": robot_target_pose[2], self._last_action[f"{self.prefix}pose.rx"] = robot_target_pose[3]
"pose.rx": robot_target_pose[3], self._last_action[f"{self.prefix}pose.ry"] = robot_target_pose[4]
"pose.ry": robot_target_pose[4], self._last_action[f"{self.prefix}pose.rz"] = robot_target_pose[5]
"pose.rz": robot_target_pose[5], else:
} pass
if self.config.use_gripper: if self.config.use_gripper:
action_dict.update({"gripper.pos": gripper_target_pos}) distance = min(max(self.pika_sense.get_gripper_distance(), 0), 100)
if distance is not None:
return action_dict gripper_pos = (100 - distance) / (100 - 0)
else:
gripper_pos = 0.0
self._last_action.update({f"{self.prefix}gripper.pos": gripper_pos})
return self._last_action
def send_feedback(self, feedback: dict[str, float]) -> None: def send_feedback(self, feedback: dict[str, float]) -> None:
raise NotImplementedError raise NotImplementedError

View File

@ -15,18 +15,20 @@
# limitations under the License. # limitations under the License.
from dataclasses import dataclass from dataclasses import dataclass
from typing import Tuple
from lerobot.teleoperators import TeleoperatorConfig from lerobot.teleoperators import TeleoperatorConfig
@TeleoperatorConfig.register_subclass("uf::pika_teleop") @TeleoperatorConfig.register_subclass("uf::pika_teleop")
@dataclass @dataclass
class PikaTeleopConfig(TeleoperatorConfig): class PikaTeleopConfig(TeleoperatorConfig):
# robot_ip to connect to the arm
robot_ip: str = None
# Port to connect to the pika # Port to connect to the pika
port: str = None port: str = None
frequency: int = 100 # hz frequency: int = 100 # hz
use_gripper: bool = True use_gripper: bool = True
scale_xyz: float = 1.0 # scale_xyz: float = 1.0
rx_continuous: bool = False tracker_to_robot_eef: Tuple[float, ...] = (0, 0, 0, 180, -90, 0) # [x, y, z, roll(°), pitch(°), yaw(°)]
robot_base_pose: Tuple[float, ...] = (400, 0, 400, 180, 0, 0) # [x, y, z, roll(°), pitch(°), yaw(°)]
def __post_init__(self):
self.id = 'pika_teleop' if self.id is None else self.id

View File

@ -29,3 +29,6 @@ class SpaceMouseTeleopConfig(TeleoperatorConfig):
frequency: int = 10 # hz frequency: int = 10 # hz
max_pos_speed: int = 250 # mm/s max_pos_speed: int = 250 # mm/s
# Others: Calibration angles, joint directions etc. # Others: Calibration angles, joint directions etc.
def __post_init__(self):
self.id = 'spacemouse_teleop' if self.id is None else self.id

View File

@ -46,3 +46,6 @@ class UFMockTeleopConfig(TeleoperatorConfig):
gripper_freq: int = 50 # Hz gripper_freq: int = 50 # Hz
gripper_open: int = 800 gripper_open: int = 800
gripper_close: int = 0 gripper_close: int = 0
def __post_init__(self):
self.id = 'uf_mock_teleop' if self.id is None else self.id

View File

@ -55,9 +55,13 @@ class MultipleUmiTeleop(UFBaseTeleop):
for teleop in self.teleops.values(): for teleop in self.teleops.values():
teleop.disconnect() teleop.disconnect()
def set_ctrl_status(self, status): def set_teleop_enabled(self, enabled: bool, obs=None):
for teleop in self.teleops.values(): for key, teleop in self.teleops.items():
teleop.set_ctrl_status(status) if obs is not None:
teleop_obs = {k: v for k, v in obs.items() if k.startswith(f"{key}.")}
else:
teleop_obs = None
teleop.set_teleop_enabled(enabled, teleop_obs)
def get_action(self) -> dict[str, Any]: def get_action(self) -> dict[str, Any]:
actions = {} actions = {}

View File

@ -22,4 +22,7 @@ from .umi_teleop_config import UmiTeleopConfig
@TeleoperatorConfig.register_subclass("uf::multiple_umi_teleop") @TeleoperatorConfig.register_subclass("uf::multiple_umi_teleop")
@dataclass @dataclass
class MultipleUmiTeleopConfig(TeleoperatorConfig): class MultipleUmiTeleopConfig(TeleoperatorConfig):
teleops: dict[str, UmiTeleopConfig] teleops: dict[str, UmiTeleopConfig]
def __post_init__(self):
self.id = 'multiple_umi_teleop' if self.id is None else self.id

View File

@ -1,8 +1,8 @@
#!/usr/bin/env python #!/usr/bin/env python
import math
from typing import Any from typing import Any
from lerobot.utils.errors import DeviceAlreadyConnectedError, DeviceNotConnectedError from lerobot.utils.errors import DeviceAlreadyConnectedError, DeviceNotConnectedError
from ufactory_lerobot.devices.umi.vive_tracker.transformations import Transformations from ufactory_lerobot.devices.umi.vive_tracker.transformations import Transformations
from ufactory_lerobot.devices.umi.vive_tracker import ViveTracker from ufactory_lerobot.devices.umi.vive_tracker import ViveTracker
from ufactory_lerobot.devices.umi.xvlib import XVLib from ufactory_lerobot.devices.umi.xvlib import XVLib
@ -21,31 +21,22 @@ class UmiTeleop(UFBaseTeleop):
self.prefix = '' if not prefix else f'{prefix}.' self.prefix = '' if not prefix else f'{prefix}.'
self._is_connected = False self._is_connected = False
self._is_calibrated = True self._is_calibrated = True
self._teleop_enabled = False
self._last_action = None
if self.config.use_gripper: self.tracker = None
self.config.init_clamp_stream = True self.xvlib = None
else:
self.config.init_clamp_stream = False
self.tracker = ViveTracker() if self.config.use_vive_tracker else None # self.tracker = ViveTracker() if self.config.use_vive_tracker else None
self.xvlib = XVLib(self.config.serial_number, self.config.init_slam, self.config.init_clamp_stream, self.config.init_color_camera, self.config.init_fisheye_cameras) # self.xvlib = XVLib(self.config.serial_number, not self.config.use_vive_tracker, self.config.use_gripper)
# tracker_to_robot_eef = [0, 0, 0, math.pi / 2, -math.pi / 2, 0] tracker_to_robot_eef = list(self.config.tracker_to_robot_eef[:3]) + list(map(math.radians, self.config.tracker_to_robot_eef[3:6]))
# tracker_to_robot_eef = [0, 0, 0, 0, 0, -math.pi/2]
# tracker_to_robot_eef = [0, 0, 0, math.pi, math.pi, 0] # Test1
# tracker_to_robot_eef = [0, 0, 0, math.pi, math.pi, -math.pi/2] # Dual left
# tracker_to_robot_eef = [0, 0, 0, math.pi, math.pi, math.pi/2] # Dual right
tracker_to_robot_eef = self.config.tracker_to_robot_eef
self.tracker_to_robot_matrix = Transformations.xyzrpy_to_rotation_matrix(*tracker_to_robot_eef) self.tracker_to_robot_matrix = Transformations.xyzrpy_to_rotation_matrix(*tracker_to_robot_eef)
# robot_base_pose = [300, 0, 300, 0, 0, 0] robot_base_pose = list(self.config.robot_base_pose[:3]) + list(map(math.radians, self.config.robot_base_pose[3:6]))
# robot_base_pose = [300, 0, 300, math.pi, -math.pi/2, 0]
# robot_base_pose = [220, 0, 385, math.pi, 0, 0]
# robot_base_pose = [250, 0, 150, math.pi, 0, 0] # Test 1
# robot_base_pose = [250, 0, 150, math.pi, 0, math.pi/2] # Dual left
# robot_base_pose = [250, 0, 150, math.pi, 0, math.pi/2] # Dual right
robot_base_pose = self.config.robot_base_pose
self.robot_base_matrix = Transformations.xyzrpy_to_rotation_matrix(*robot_base_pose) self.robot_base_matrix = Transformations.xyzrpy_to_rotation_matrix(*robot_base_pose)
self.begin_tracker_robot_matrix = None self.begin_tracker_robot_matrix = None
self._last_robot_pose = Transformations.rotation_matrix_to_xyzrxryrz(self.robot_base_matrix)
self._last_gripper_pos = 0.0
@property @property
def action_features(self) -> dict: def action_features(self) -> dict:
@ -93,11 +84,13 @@ class UmiTeleop(UFBaseTeleop):
pass pass
def connect(self, calibrate: bool = False) -> None: def connect(self, calibrate: bool = False) -> None:
self.xvlib.xv_init(self.config.serial_number, self.config.init_slam, self.config.init_clamp_stream, self.config.init_color_camera, self.config.init_fisheye_cameras) self.tracker = ViveTracker() if self.config.use_vive_tracker else None
self.xvlib = XVLib(self.config.serial_number, not self.config.use_vive_tracker, self.config.use_gripper)
self._is_connected = True self._is_connected = True
def disconnect(self): def disconnect(self):
self.xvlib.xv_uninit() if self.xvlib:
self.xvlib.xv_uninit()
self._is_connected = False self._is_connected = False
@staticmethod @staticmethod
@ -111,11 +104,26 @@ class UmiTeleop(UFBaseTeleop):
angle_deg += 360 angle_deg += 360
return angle_deg return angle_deg
def set_ctrl_status(self, status): def set_teleop_enabled(self, enabled: bool, obs=None):
if status: if enabled:
if obs is not None:
self._last_robot_pose = [obs[f"{self.prefix}pose.x"], obs[f"{self.prefix}pose.y"], obs[f"{self.prefix}pose.z"], obs[f"{self.prefix}pose.rx"], obs[f"{self.prefix}pose.ry"], obs[f"{self.prefix}pose.rz"]]
if self.config.use_gripper:
self._last_gripper_pos = obs[f"{self.prefix}gripper.pos"]
self.robot_base_matrix = Transformations.xyzrxryrz_to_rotation_matrix(*self._last_robot_pose)
self.begin_tracker_robot_matrix = None self.begin_tracker_robot_matrix = None
self._last_action = None
self._teleop_enabled = True
print(f'[{self.prefix}UMI] Teleoperation is start')
else: else:
pass obs = self._last_action
if obs:
self._last_robot_pose = [obs[f"{self.prefix}pose.x"], obs[f"{self.prefix}pose.y"], obs[f"{self.prefix}pose.z"], obs[f"{self.prefix}pose.rx"], obs[f"{self.prefix}pose.ry"], obs[f"{self.prefix}pose.rz"]]
if self.config.use_gripper:
self._last_gripper_pos = obs[f"{self.prefix}gripper.pos"]
self._teleop_enabled = False
self._last_action = None
print(f'[{self.prefix}UMI] Teleoperation has paused')
# delta action # delta action
def get_action(self) -> dict[str, Any]: def get_action(self) -> dict[str, Any]:
@ -124,6 +132,20 @@ class UmiTeleop(UFBaseTeleop):
"UmiTeleop is not connected. You need to run `connect()` before `get_action()`." "UmiTeleop is not connected. You need to run `connect()` before `get_action()`."
) )
if self._last_action is None:
self._last_action = {
f"{self.prefix}pose.x": self._last_robot_pose[0],
f"{self.prefix}pose.y": self._last_robot_pose[1],
f"{self.prefix}pose.z": self._last_robot_pose[2],
f"{self.prefix}pose.rx": self._last_robot_pose[3],
f"{self.prefix}pose.ry": self._last_robot_pose[4],
f"{self.prefix}pose.rz": self._last_robot_pose[5],
}
if self.config.use_gripper:
self._last_action.update({f"{self.prefix}gripper.pos": self._last_gripper_pos})
if not self._teleop_enabled:
return self._last_action
if self.tracker is not None: if self.tracker is not None:
pose_data = self.tracker.get_pose(self.config.vive_tracker_id) pose_data = self.tracker.get_pose(self.config.vive_tracker_id)
if pose_data is None: if pose_data is None:
@ -133,25 +155,6 @@ class UmiTeleop(UFBaseTeleop):
_, pose_data = self.xvlib.xv_get_slam_data() _, pose_data = self.xvlib.xv_get_slam_data()
position = pose_data.position.to_list(6) position = pose_data.position.to_list(6)
quaternion = pose_data.quaternion.to_list(6) quaternion = pose_data.quaternion.to_list(6)
# orientation = pose_data.orientation.to_list()
# x, y, z = position[0] * 1000, position[1] * 1000, position[2] * 1000
# roll, pitch, yaw = math.degrees(orientation[0]), math.degrees(orientation[1]), math.degrees(orientation[2])
# print(f'[1] x={x:.1f}, y={y:.1f}, z={z:.1f}, roll={roll:.1f}, pitch={pitch:.1f}, yaw={yaw:.1f}')
# x, y, z = position[2] * 1000, position[0] * 1000, position[1] * 1000
# roll, pitch, yaw = orientation[0], orientation[1], orientation[2]
# roll, pitch, yaw = math.degrees(roll), math.degrees(pitch), math.degrees(yaw)
# print(f'[1] x={x:.1f}, y={y:.1f}, z={z:.1f}, roll={roll:.1f}, pitch={pitch:.1f}, yaw={yaw:.1f}')
# x, y, z = position[0] * 1000, position[1] * 1000, position[2] * 1000
# R_A = Transformations.quaternion_to_rotation_matrix(quaternion)
# roll, pitch, yaw = Transformations.rotation_matrix_to_rpy(R_A)
# roll, pitch, yaw = math.degrees(roll), math.degrees(pitch), math.degrees(yaw)
# print(f'[2] x={x:.1f}, y={y:.1f}, z={z:.1f}, roll={roll:.1f}, pitch={pitch:.1f}, yaw={yaw:.1f}')
# print('*' * 50)
# roll, pitch, yaw = math.degrees(orientation[0]), math.degrees(orientation[1]), math.degrees(orientation[2])
# print(f'[1] x={x:.1f}, y={y:.1f}, z={z:.1f}, roll={roll:.1f}, pitch={pitch:.1f}, yaw={yaw:.1f}')
x, y, z = position[0] * 1000, position[1] * 1000, position[2] * 1000 x, y, z = position[0] * 1000, position[1] * 1000, position[2] * 1000
tracker_robot_matrix = Transformations.tracker_pose_to_robot_matrix(x, y, z, quaternion, self.tracker_to_robot_matrix) tracker_robot_matrix = Transformations.tracker_pose_to_robot_matrix(x, y, z, quaternion, self.tracker_to_robot_matrix)
@ -159,43 +162,19 @@ class UmiTeleop(UFBaseTeleop):
self.begin_tracker_robot_matrix = tracker_robot_matrix self.begin_tracker_robot_matrix = tracker_robot_matrix
robot_target_pose = Transformations.tracker_robot_matrix_to_robot_pose(self.begin_tracker_robot_matrix, tracker_robot_matrix, self.robot_base_matrix, is_axis_angle=True) robot_target_pose = Transformations.tracker_robot_matrix_to_robot_pose(self.begin_tracker_robot_matrix, tracker_robot_matrix, self.robot_base_matrix, is_axis_angle=True)
x, y, z = robot_target_pose[0:3] self._last_action[f"{self.prefix}pose.x"] = robot_target_pose[0]
orientation = robot_target_pose[3:6] self._last_action[f"{self.prefix}pose.y"] = robot_target_pose[1]
# roll, pitch, yaw = list(map(math.degrees, orientation)) self._last_action[f"{self.prefix}pose.z"] = robot_target_pose[2]
# print(f'[{self.config.serial_number}] x={x:.3f}, y={y:.3f}, z={z:.3f}, rx={roll:.3f}, ry={pitch:.3f}, rz={yaw:.3f}') self._last_action[f"{self.prefix}pose.rx"] = robot_target_pose[3]
self._last_action[f"{self.prefix}pose.ry"] = robot_target_pose[4]
# R_prev = Transformations.rpy_to_rotation_matrix(math.pi, -math.pi / 2, 0) self._last_action[f"{self.prefix}pose.rz"] = robot_target_pose[5]
# R_delta = Transformations.rxryrz_to_matrix(robot_target_pose[3:6])
# R_curr = R_prev @ R_delta
# # # R_curr = R_prev.apply(R_delta)
# orientation = Transformations.rotation_matrix_to_rxryrz(R_curr)
# roll, pitch, yaw = math.degrees(orientation[0]), math.degrees(orientation[1]), math.degrees(orientation[2])
# print(f'[2] x={x:.1f}, y={y:.1f}, z={z:.1f}, rx={roll:.1f}, ry={pitch:.1f}, rz={yaw:.1f}')
# print('*' * 50)
# output is delta change of the robot pose
action_dict = {
# "pose.x": z,
# "pose.y": -y,
# "pose.z": x,
# "pose.rx": orientation[2],
# "pose.ry": -orientation[1],
# "pose.rz": orientation[0],
f"{self.prefix}pose.x": x,
f"{self.prefix}pose.y": y,
f"{self.prefix}pose.z": z,
f"{self.prefix}pose.rx": orientation[0],
f"{self.prefix}pose.ry": orientation[1],
f"{self.prefix}pose.rz": orientation[2],
}
if self.config.use_gripper: if self.config.use_gripper:
_, clamp_data = self.xvlib.xv_get_clamp_stream_data() _, clamp_data = self.xvlib.xv_get_clamp_stream_data()
gripper_pos = (87 - clamp_data.data) / (87 - 0) gripper_pos = (87 - clamp_data.data) / (87 - 0)
action_dict.update({f"{self.prefix}gripper.pos": gripper_pos}) self._last_action.update({f"{self.prefix}gripper.pos": gripper_pos})
return action_dict return self._last_action
def send_feedback(self, feedback: dict[str, float]) -> None: def send_feedback(self, feedback: dict[str, float]) -> None:
raise NotImplementedError raise NotImplementedError

View File

@ -16,7 +16,6 @@
from dataclasses import dataclass from dataclasses import dataclass
from typing import Tuple from typing import Tuple
import numpy as np
from lerobot.teleoperators import TeleoperatorConfig from lerobot.teleoperators import TeleoperatorConfig
@ -24,12 +23,11 @@ from lerobot.teleoperators import TeleoperatorConfig
@dataclass @dataclass
class UmiTeleopConfig(TeleoperatorConfig): class UmiTeleopConfig(TeleoperatorConfig):
serial_number: str serial_number: str
init_slam: bool = True
init_clamp_stream: bool = True
init_color_camera: bool = False
init_fisheye_cameras: bool = False
use_gripper: bool = True use_gripper: bool = True
use_vive_tracker: bool = False use_vive_tracker: bool = False
vive_tracker_id: str = 'WM0' vive_tracker_id: str = 'WM0'
tracker_to_robot_eef: Tuple[float, ...] = (0, 0, 0, 0, 0, -np.pi/2) tracker_to_robot_eef: Tuple[float, ...] = (0, 0, 0, 0, 0, -90) # [x, y, z, roll(°), pitch(°), yaw(°)]
robot_base_pose: Tuple[float, ...] = (300, 0, 300, np.pi, -np.pi/2, 0) robot_base_pose: Tuple[float, ...] = (300, 0, 300, 180, -90, 0) # [x, y, z, roll(°), pitch(°), yaw(°)]
def __post_init__(self):
self.id = 'umi_teleop' if self.id is None else self.id

View File

@ -1,4 +1,6 @@
import logging
import importlib import importlib
from lerobot.utils.control_utils import is_headless
# recursive call, inspired from gello_software: # recursive call, inspired from gello_software:
def instantiate_from_dict(cfg, ignore_cameras=False): def instantiate_from_dict(cfg, ignore_cameras=False):
@ -16,3 +18,57 @@ def instantiate_from_dict(cfg, ignore_cameras=False):
return [instantiate_from_dict(v, ignore_cameras) for v in cfg] return [instantiate_from_dict(v, ignore_cameras) for v in cfg]
else: else:
return cfg return cfg
def init_keyboard_listener(events: dict = None, on_press: callable = None, on_release: callable = None):
"""
Initializes a non-blocking keyboard listener for real-time user interaction.
This function sets up a listener for specific keys (right arrow, left arrow, escape) to control
the program flow during execution, such as stopping recording or exiting loops. It gracefully
handles headless environments where keyboard listening is not possible.
Returns:
A tuple containing:
- The `pynput.keyboard.Listener` instance, or `None` if in a headless environment.
- A dictionary of event flags (e.g., `exit_early`) that are set by key presses.
"""
# Allow to exit early while recording an episode or resetting the environment,
# by tapping the right arrow key '->'. This might require a sudo permission
# to allow your terminal to monitor keyboard events.
if events is None:
events = {}
events["exit_early"] = False
events["rerecord_episode"] = False
events["stop_recording"] = False
if is_headless():
logging.warning(
"Headless environment detected. On-screen cameras display and keyboard inputs will not be available."
)
listener = None
return listener, events
# Only import pynput if not in a headless environment
from pynput import keyboard
if on_press is None:
def on_press(key):
try:
if key == keyboard.Key.right:
print("Right arrow key pressed. Exiting loop...")
events["exit_early"] = True
elif key == keyboard.Key.left:
print("Left arrow key pressed. Exiting loop and rerecord the last episode...")
events["rerecord_episode"] = True
events["exit_early"] = True
elif key == keyboard.Key.esc:
print("Escape key pressed. Stopping data recording...")
events["stop_recording"] = True
events["exit_early"] = True
except Exception as e:
print(f"Error handling key press: {e}")
listener = keyboard.Listener(on_press=on_press, on_release=on_release)
listener.start()
return listener, events