Add gello and robot reset pose logic.

This commit is contained in:
Saberlve 2026-08-07 15:56:54 +08:00
parent b51dfa128a
commit 2f7077fcbf
20 changed files with 389 additions and 66 deletions

View File

@ -5,7 +5,6 @@ robot:
control_space: "joint" control_space: "joint"
robot_ip: "192.168.1.245" robot_ip: "192.168.1.245"
gripper_type: 1 gripper_type: 1
start_joints: [0, 0, -90, 90, 0]
# make sure to edit with your correct configurations! # make sure to edit with your correct configurations!
teleop: teleop:
@ -15,7 +14,6 @@ teleop:
joint_signs: [1, 1, -1, 1, 1] joint_signs: [1, 1, -1, 1, 1]
start_joints: [0, 0, -90, 90, 0] start_joints: [0, 0, -90, 90, 0]
gripper_id: 8 gripper_id: 8
torque_joint_ids: [3, 5]
dataset: dataset:
# root of local repo: /home/<user_name>/.cache/huggingface/lerobot (default) # root of local repo: /home/<user_name>/.cache/huggingface/lerobot (default)

View File

@ -5,7 +5,6 @@ robot:
control_space: "joint" control_space: "joint"
robot_ip: "192.168.1.245" robot_ip: "192.168.1.245"
gripper_type: 1 gripper_type: 1
start_joints: [0, 0, -90, 0, 90, 0]
# make sure to edit with your correct configurations! # make sure to edit with your correct configurations!
teleop: teleop:
@ -15,7 +14,6 @@ teleop:
joint_signs: [1, 1, -1, 1, 1, 1] joint_signs: [1, 1, -1, 1, 1, 1]
start_joints: [0, 0, -90, 0, 90, 0] start_joints: [0, 0, -90, 0, 90, 0]
gripper_id: 8 gripper_id: 8
torque_joint_ids: [3]
dataset: dataset:
# root of local repo: /home/<user_name>/.cache/huggingface/lerobot (default) # root of local repo: /home/<user_name>/.cache/huggingface/lerobot (default)

View File

@ -5,14 +5,16 @@ robot:
control_space: "joint" control_space: "joint"
robot_ip: "192.168.1.245" robot_ip: "192.168.1.245"
gripper_type: 1 gripper_type: 1
start_joints: [0, 0, 0, 90, 0, 90, 0] # Redundant args, indicating the initial pose of xarm7. Set by 192.168.1.245:18333
# start_joints: [0, -30, 0, 0, 0, 30, 0]
# make sure to edit with your correct configurations! # make sure to edit with your correct configurations!
teleop: teleop:
type: uf::gello_teleop type: uf::gello_teleop
id: "gello_teleop" id: "gello_teleop"
port: "/dev/serial/by-id/usb-FTDI_USB__-__Serial_Converter_FTAJZYC7-if00-port0" port: "/dev/serial/by-id/usb-FTDI_USB__-__Serial_Converter_FTAJZYC7-if00-port0"
start_joints: [0, 0, 0, 90, 0, 90, 0] # GELLO calibration reference; matches the xArm SDK initial point.
start_joints: [0, -30, 0, 0, 0, 30, 0]
dataset: dataset:
# root of local repo: /home/<user_name>/.cache/huggingface/lerobot (default) # root of local repo: /home/<user_name>/.cache/huggingface/lerobot (default)

View File

@ -6,8 +6,6 @@ robot:
robot_ip: "192.168.1.85" robot_ip: "192.168.1.85"
gripper_type: 10 gripper_type: 10
max_linear_velocity: 200 max_linear_velocity: 200
start_joints: [-0.2, -13.9, -0.3, 52.6, 0.9, 66.6, 0.2]
start_tcp_pose: [400, 0, 400, 180, 0, 0]
cameras: cameras:
fisheye: fisheye:
type: opencv type: opencv

View File

@ -13,8 +13,6 @@ robot:
robot_ip: "192.168.1.29" robot_ip: "192.168.1.29"
gripper_type: 2 gripper_type: 2
max_linear_velocity: 200 max_linear_velocity: 200
start_joints: [-14.7, 25.6, -49.6, -282.5, 95.9, -65.9]
start_tcp_pose: [400, 0, 150, -90, 0, 0]
cameras: cameras:
fisheye: fisheye:
type: opencv type: opencv
@ -31,8 +29,6 @@ robot:
robot_ip: "192.168.1.83" robot_ip: "192.168.1.83"
gripper_type: 2 gripper_type: 2
max_linear_velocity: 200 max_linear_velocity: 200
start_joints: [14.5, 25.5, -49.6, -76.8, 95.9, 66.6]
start_tcp_pose: [400, 0, 150, 90, 0, 0]
cameras: cameras:
fisheye: fisheye:
type: opencv type: opencv
@ -75,4 +71,3 @@ dataset:
num_episodes: 100 num_episodes: 100
push_to_hub: False push_to_hub: False

View File

@ -6,8 +6,6 @@ robot:
robot_ip: "192.168.1.83" robot_ip: "192.168.1.83"
gripper_type: 2 gripper_type: 2
max_linear_velocity: 250 max_linear_velocity: 250
start_joints: [0, -14.8, -52.6, 0, 67.4, 0]
start_tcp_pose: [400, 0, 400, 180, 0, 0]
cameras: cameras:
fisheye: fisheye:
type: opencv type: opencv
@ -35,4 +33,4 @@ dataset:
episode_time_s: 60 # max duration for one episode episode_time_s: 60 # max duration for one episode
reset_time_s: 20 # time for resetting env between episodes reset_time_s: 20 # time for resetting env between episodes
num_episodes: 100 num_episodes: 100
push_to_hub: False push_to_hub: False

View File

@ -57,6 +57,10 @@ class MultipleUFMockRobot(Robot):
for robot in self.robots.values(): for robot in self.robots.values():
robot.configure() robot.configure()
def reset_to_initial(self) -> None:
for robot in self.robots.values():
robot.reset_to_initial()
def disconnect(self) -> None: def disconnect(self) -> None:
for robot in self.robots.values(): for robot in self.robots.values():
robot.disconnect() robot.disconnect()

View File

@ -93,6 +93,9 @@ class UFMockRobot(Robot):
def configure(self) -> None: def configure(self) -> None:
pass pass
def reset_to_initial(self) -> None:
pass
def calibrate(self) -> None: def calibrate(self) -> None:
self._is_calibrated = True self._is_calibrated = True
pass # CHECK! currently No-op pass # CHECK! currently No-op
@ -130,4 +133,3 @@ class UFMockRobot(Robot):
return self._is_connected return self._is_connected

View File

@ -101,6 +101,30 @@ class MultipleUFRobot(Robot):
for robot in self.robots.values(): for robot in self.robots.values():
robot.configure() robot.configure()
def reset_to_initial(self) -> None:
"""Reset every arm to its SDK-configured initial point."""
if self._is_async_configure:
errors = []
def reset_robot(robot):
try:
robot.reset_to_initial()
except BaseException as exc:
errors.append(exc)
threads = []
for robot in self.robots.values():
thread = threading.Thread(target=reset_robot, args=(robot,), daemon=True)
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
if errors:
raise errors[0]
else:
for robot in self.robots.values():
robot.reset_to_initial()
def disconnect(self) -> None: def disconnect(self) -> None:
for robot in self.robots.values(): for robot in self.robots.values():
robot.disconnect() robot.disconnect()

View File

@ -17,6 +17,7 @@ from xarm.core.utils import convert
## Configurations: ## Configurations:
INIT_SYNC_JOINT_VELOCITY_RAD = 0.2 INIT_SYNC_JOINT_VELOCITY_RAD = 0.2
ROBOT_RESET_SPEED_DEG = 60
CARTESIAN_OBS_KEYS = [ CARTESIAN_OBS_KEYS = [
"pose.x", "pose.y", "pose.z", "pose.rx", "pose.ry", "pose.rz", "pose.x", "pose.y", "pose.z", "pose.rx", "pose.ry", "pose.rz",
@ -75,6 +76,7 @@ class UFRobot(Robot, Thread):
self._control_space = self.config.control_space self._control_space = self.config.control_space
self.real_arm = None self.real_arm = None
self._initial_point = None
cameras_args = self.config.cameras_args or {} cameras_args = self.config.cameras_args or {}
self.camera_width = cameras_args.get('w', 0) self.camera_width = cameras_args.get('w', 0)
self.camera_height = cameras_args.get('h', 0) self.camera_height = cameras_args.get('h', 0)
@ -90,15 +92,6 @@ 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
if self.config.start_tcp_pose and len(self.config.start_tcp_pose) >= 6:
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
self._update_lock = Lock() self._update_lock = Lock()
@ -193,6 +186,13 @@ class UFRobot(Robot, Thread):
self._is_connected = False self._is_connected = False
raise ConnectionError() raise ConnectionError()
code, initial_point = self.real_arm.get_initial_point()
if code != 0:
raise RuntimeError(f"get_initial_point failed, code={code}")
if initial_point is None or len(initial_point) < self._dof:
raise RuntimeError(f"Invalid initial point returned by xArm: {initial_point}")
self._initial_point = list(initial_point[:self._dof])
for cam in self.cameras.values(): for cam in self.cameras.values():
cam.connect() cam.connect()
self._is_connected = self._is_connected and cam.is_connected self._is_connected = self._is_connected and cam.is_connected
@ -206,7 +206,10 @@ class UFRobot(Robot, Thread):
# print('Could not connect to pika gripper.') # print('Could not connect to pika gripper.')
# raise ConnectionError() # raise ConnectionError()
self.configure() if self.config.manual_mode:
self.configure()
else:
self.reset_to_initial()
if calibrate: if calibrate:
self.calibrate() self.calibrate()
@ -214,6 +217,25 @@ class UFRobot(Robot, Thread):
self._is_connected = True self._is_connected = True
def reset_to_initial(self) -> None:
if not self._is_connected or self.real_arm is None:
raise ConnectionError("UF Robot is not connected")
if self._initial_point is None:
raise RuntimeError("xArm initial point has not been loaded")
self.real_arm.set_mode(0)
self.real_arm.set_state(0)
code = self.real_arm.set_servo_angle(
angle=self._initial_point,
speed=ROBOT_RESET_SPEED_DEG,
is_radian=False,
wait=True,
)
if code != 0:
raise RuntimeError(f"Failed to move to xArm initial point, code={code}")
self.configure()
def configure(self) -> None: def configure(self) -> None:
self.real_arm.motion_enable() self.real_arm.motion_enable()
self.real_arm.clean_error() self.real_arm.clean_error()
@ -277,13 +299,6 @@ 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": if self._control_space == "joint":
self.real_arm.set_mode(6) self.real_arm.set_mode(6)
elif self._control_space == "cartesian": elif self._control_space == "cartesian":

View File

@ -1,5 +1,4 @@
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Tuple
from lerobot.cameras import CameraConfig from lerobot.cameras import CameraConfig
from lerobot.robots import RobotConfig from lerobot.robots import RobotConfig
@ -20,8 +19,8 @@ class UFRobotConfig(RobotConfig):
observe_joint_vel: bool = False # only effective in joint control mode observe_joint_vel: bool = False # only effective in joint control mode
manual_mode: bool = False # xArm joint teaching mode; records state without sending actions manual_mode: bool = False # xArm joint teaching mode; records state without sending actions
teach_sensitivity: int | None = None # xArm teaching sensitivity, valid range: 1-5 teach_sensitivity: int | None = None # xArm teaching sensitivity, valid range: 1-5
start_joints: Tuple[float, ...] = (0, 0, 0, 90, 0, 90, 0) # ° # start_joints and start_tcp_pose are intentionally disabled.
start_tcp_pose: Tuple[float, ...] = None # [x, y, z, roll(°), pitch(°), yaw(°)] # Reset uses the xArm SDK initial_point instead of configuration poses.
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
no_action: bool = False # only for debug no_action: bool = False # only for debug

View File

@ -232,7 +232,10 @@ def eval_loop(cfg: EvalConfig, relative=False, 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() reset = getattr(robot, "reset_to_initial", None)
if reset is None:
reset = robot.configure
reset()
policy.reset() policy.reset()
preprocessor.reset() preprocessor.reset()
postprocessor.reset() postprocessor.reset()

View File

@ -408,6 +408,23 @@ def record_loop(
timestamp = time.perf_counter() - start_episode_t timestamp = time.perf_counter() - start_episode_t
def _prepare_recording_episode(robot, teleop, is_uf_teleop, manual_mode):
if is_uf_teleop:
# Stop teleop output before handing control to the xArm reset motion.
teleop.set_teleop_enabled(False)
if is_uf_teleop or manual_mode:
reset = getattr(robot, "reset_to_initial", None)
if reset is None:
reset = robot.configure
reset()
if is_uf_teleop:
obs = robot.get_observation()
teleop.reset_to_robot_observation(obs)
teleop.set_teleop_enabled(True, obs)
def record(cfg: UFRecordConfig, async_save: bool = False) -> LeRobotDataset: def record(cfg: UFRecordConfig, async_save: bool = False) -> LeRobotDataset:
init_logging() init_logging()
logging.info(pformat(asdict(cfg))) logging.info(pformat(asdict(cfg)))
@ -540,8 +557,6 @@ def record(cfg: UFRecordConfig, async_save: bool = False) -> LeRobotDataset:
print('⌨ [ESC] Exit [Space] Start [←] Reset [→] Save') print('⌨ [ESC] Exit [Space] Start [←] Reset [→] Save')
else: else:
input('⌨ Press Enter to start record >>> ') input('⌨ Press Enter to start record >>> ')
if is_uf_teleop:
teleop.set_teleop_enabled(True)
is_recorded = True is_recorded = True
print('\n********** Episode Record Loop Start **********') print('\n********** Episode Record Loop Start **********')
@ -575,10 +590,7 @@ def record(cfg: UFRecordConfig, async_save: bool = False) -> LeRobotDataset:
events["rerecord_episode"] = False events["rerecord_episode"] = False
events["exit_early"] = False events["exit_early"] = False
if is_uf_teleop or manual_mode: if is_uf_teleop or manual_mode:
robot.configure() _prepare_recording_episode(robot, teleop, is_uf_teleop, manual_mode)
if is_uf_teleop:
obs = robot.get_observation()
teleop.set_teleop_enabled(True, obs)
log_say(f"Recording episode {_current_episode_index(dataset)}", cfg.play_sounds) log_say(f"Recording episode {_current_episode_index(dataset)}", cfg.play_sounds)
record_loop( record_loop(
robot=robot, robot=robot,

View File

@ -61,7 +61,20 @@ def teleop_loop(cfg: TeleopConfig):
is_evt = not is_headless() is_evt = not is_headless()
is_uf_teleop = isinstance(teleop, UFBaseTeleop) is_uf_teleop = isinstance(teleop, UFBaseTeleop)
is_reset = False def reset_uf_control():
if is_uf_teleop:
# Stop teleop output before handing control to the xArm reset motion.
teleop.set_teleop_enabled(False)
reset = getattr(robot, "reset_to_initial", None)
if reset is None:
reset = robot.configure
reset()
if is_uf_teleop:
obs = robot.get_observation()
teleop.reset_to_robot_observation(obs)
teleop.set_teleop_enabled(True, obs)
is_reset = is_uf_teleop
is_paused = True is_paused = True
events = {"exit": False} events = {"exit": False}
listener = None listener = None
@ -105,12 +118,14 @@ def teleop_loop(cfg: TeleopConfig):
listener, events = init_keyboard_listener(events=events, on_press=on_press, on_release=on_release) listener, events = init_keyboard_listener(events=events, on_press=on_press, on_release=on_release)
print("\n********** Teleop Control Loop Start **********") print("\n********** Teleop Control Loop Start **********")
print('⌨ [ESC] Exit [Space] Start [←] Reset') if is_uf_teleop:
print('⌨ [ESC] Exit [Space] Reset / Start [←] Reset')
else:
print('⌨ [ESC] Exit [Space] Start [←] Reset')
else: else:
input('⌨ Press Enter to start teleop >>> ') input('⌨ Press Enter to start teleop >>> ')
if is_uf_teleop: if is_uf_teleop:
obs = robot.get_observation() reset_uf_control()
teleop.set_teleop_enabled(True, obs)
is_paused = False is_paused = False
is_reset = False is_reset = False
print("\n********** Teleop Control Loop Start **********") print("\n********** Teleop Control Loop Start **********")
@ -143,10 +158,10 @@ def teleop_loop(cfg: TeleopConfig):
print('⌨ [ESC] Exit [Space] Start [←] Reset') print('⌨ [ESC] Exit [Space] Start [←] Reset')
else: else:
if is_reset: if is_reset:
reset_uf_control()
is_reset = False is_reset = False
robot.configure()
# print('========== Teleop is start ==========') # print('========== Teleop is start ==========')
if is_uf_teleop: elif is_uf_teleop:
obs = robot.get_observation() obs = robot.get_observation()
teleop.set_teleop_enabled(True, obs) teleop.set_teleop_enabled(True, obs)
print('⌨ [ESC] Exit [Space] Pause [←] Reset') print('⌨ [ESC] Exit [Space] Pause [←] Reset')

View File

@ -22,3 +22,7 @@ class UFBaseTeleop(Teleoperator):
当enabled为True且obs不为None时, 顺便设置机械臂初始位置映射 当enabled为True且obs不为None时, 顺便设置机械臂初始位置映射
""" """
pass pass
def reset_to_robot_observation(self, obs):
"""Align a physical teleoperator with the robot after robot reset."""
pass

View File

@ -10,6 +10,11 @@ from .gello_teleop_config import GelloTeleopConfig
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
GELLO_RESET_SPEED_DEG = 30.0
GELLO_RESET_TOLERANCE_DEG = 2.0
GELLO_RESET_CONTROL_HZ = 50.0
GELLO_RESET_TIMEOUT_MARGIN_S = 5.0
class GelloTeleop(UFBaseTeleop): class GelloTeleop(UFBaseTeleop):
""" """
GELLO for xArm tele-op, ref: https://wuphilipp.github.io/gello_site/ GELLO for xArm tele-op, ref: https://wuphilipp.github.io/gello_site/
@ -22,6 +27,7 @@ class GelloTeleop(UFBaseTeleop):
super().__init__(config) super().__init__(config)
self.config = config self.config = config
self._is_connected = False self._is_connected = False
self._teleop_enabled = False
self._is_calibrated = True # CHECK!! self._is_calibrated = True # CHECK!!
from gello.dynamixel.driver import DynamixelDriver from gello.dynamixel.driver import DynamixelDriver
@ -57,11 +63,6 @@ class GelloTeleop(UFBaseTeleop):
print(self._dynamixel_robo_config) print(self._dynamixel_robo_config)
self.dof = len(start_joints) self.dof = len(start_joints)
if self.config.torque_joint_ids:
driver = DynamixelDriver(self.config.torque_joint_ids, port=self.config.port, baudrate=57600)
driver.set_torque_mode(True)
driver.close()
@property @property
def action_features(self) -> dict: def action_features(self) -> dict:
# Add one more dof for gripper # Add one more dof for gripper
@ -95,6 +96,7 @@ class GelloTeleop(UFBaseTeleop):
from gello.agents.gello_agent import GelloAgent from gello.agents.gello_agent import GelloAgent
self.gello_agent = GelloAgent(port=self.config.port, dynamixel_config=self._dynamixel_robo_config) self.gello_agent = GelloAgent(port=self.config.port, dynamixel_config=self._dynamixel_robo_config)
self.gello_agent._robot.set_torque_mode(False)
if not self._is_calibrated and calibrate: if not self._is_calibrated and calibrate:
logger.info( logger.info(
"Mismatch between calibration values in the motor and the calibration file or no calibration file found" "Mismatch between calibration values in the motor and the calibration file or no calibration file found"
@ -117,7 +119,73 @@ class GelloTeleop(UFBaseTeleop):
# TODO: Go to sync position slowly? Can not # TODO: Go to sync position slowly? Can not
pass pass
def reset_to_robot_observation(self, obs):
"""Move the physical Gello to the robot's post-reset joint state."""
if not self._is_connected:
raise DeviceNotConnectedError("Gello teleop is not connected")
self._teleop_enabled = False
gello_robot = self.gello_agent._robot
driver = gello_robot._driver
current_raw = np.asarray(driver.get_joints(), dtype=float)
target_raw = current_raw.copy()
signs = np.asarray(gello_robot._joint_signs, dtype=float)
offsets = np.asarray(gello_robot._joint_offsets, dtype=float)
target_robot_joints = np.asarray(
[obs[f"J{i + 1}.pos"] for i in range(self.dof)], dtype=float
)
target_raw[: self.dof] = target_robot_joints * signs[: self.dof] + offsets[: self.dof]
if gello_robot.gripper_open_close is not None and len(target_raw) > self.dof:
gripper_pos = float(obs.get("gripper.pos", 0.0))
gripper_open, gripper_close = gello_robot.gripper_open_close
gripper_pos = min(max(gripper_pos, 0.0), 1.0)
target_raw[self.dof] = gripper_open + gripper_pos * (gripper_close - gripper_open)
arm_delta = np.max(np.abs(target_raw[: self.dof] - current_raw[: self.dof]))
reset_speed_rad_s = math.radians(GELLO_RESET_SPEED_DEG)
duration_s = max(0.5, float(arm_delta / reset_speed_rad_s))
deadline = time.perf_counter() + duration_s + GELLO_RESET_TIMEOUT_MARGIN_S
success = False
try:
gello_robot.set_torque_mode(True)
start_t = time.perf_counter()
while True:
elapsed_s = time.perf_counter() - start_t
progress = min(elapsed_s / duration_s, 1.0)
command = current_raw + (target_raw - current_raw) * progress
driver.set_joints(command.tolist())
if progress >= 1.0:
break
time.sleep(1.0 / GELLO_RESET_CONTROL_HZ)
while time.perf_counter() < deadline:
measured_raw = np.asarray(driver.get_joints(), dtype=float)
if np.max(np.abs(measured_raw - target_raw)) <= math.radians(GELLO_RESET_TOLERANCE_DEG):
success = True
break
driver.set_joints(target_raw.tolist())
time.sleep(1.0 / GELLO_RESET_CONTROL_HZ)
if not success:
raise RuntimeError("Gello did not reach the robot initial point before timeout")
finally:
gello_robot.set_torque_mode(False)
gello_robot._last_pos = None
def set_teleop_enabled(self, enabled: bool, obs=None):
if enabled and not self._is_connected:
raise DeviceNotConnectedError("Gello teleop is not connected")
if not enabled and self._is_connected and hasattr(self, "gello_agent"):
self.gello_agent._robot.set_torque_mode(False)
self._teleop_enabled = enabled
logger.info("Gello teleoperation %s", "enabled" if enabled else "disabled")
def get_action(self) -> dict[str, np.ndarray]: def get_action(self) -> dict[str, np.ndarray]:
if not self._teleop_enabled:
raise RuntimeError("Gello teleop is disabled")
start = time.perf_counter() start = time.perf_counter()
fake_obs = dict({"joint_state": np.array([0.0]*(self.dof+1))}) # for agent.act() argument, actually no use fake_obs = dict({"joint_state": np.array([0.0]*(self.dof+1))}) # for agent.act() argument, actually no use
action_array = self.gello_agent.act(fake_obs) # current gello joint pos as np.ndarray action_array = self.gello_agent.act(fake_obs) # current gello joint pos as np.ndarray
@ -134,8 +202,8 @@ class GelloTeleop(UFBaseTeleop):
raise NotImplementedError raise NotImplementedError
def disconnect(self) -> None: def disconnect(self) -> None:
if not self._is_connected: if hasattr(self, "gello_agent"):
DeviceNotConnectedError(f"{self} is not connected.") self.gello_agent._robot.set_torque_mode(False)
self._is_connected = False self._is_connected = False
self._teleop_enabled = False
logger.info(f"{self} disconnected.") logger.info(f"{self} disconnected.")

View File

@ -14,9 +14,10 @@ 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
# GELLO encoder calibration reference; this is not the xArm reset target.
start_joints: Tuple[float, ...] = (0, 0, 0, 90, 0, 90, 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 # deprecated; reset controls all GELLO joints.
def __post_init__(self): def __post_init__(self):
self.id = 'gello_teleop' if self.id is None else self.id self.id = 'gello_teleop' if self.id is None else self.id

View File

@ -111,10 +111,11 @@ class PikaTeleop(UFBaseTeleop, Thread):
self._teleop_enabled = True self._teleop_enabled = True
print(f'[{self.prefix}PIKA] Teleoperation is start') print(f'[{self.prefix}PIKA] Teleoperation is start')
else: else:
obs = self._last_action if self._last_action 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"]] obs = self._last_action
if self.config.use_gripper: 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._last_gripper_pos = obs[f"{self.prefix}gripper.pos"] if self.config.use_gripper:
self._last_gripper_pos = obs[f"{self.prefix}gripper.pos"]
self._teleop_enabled = False self._teleop_enabled = False
self._last_action = None self._last_action = None
print(f'[{self.prefix}PIKA] Teleoperation has paused') print(f'[{self.prefix}PIKA] Teleoperation has paused')
@ -187,4 +188,4 @@ class PikaTeleop(UFBaseTeleop, Thread):
return self._last_action 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

110
tests/test_gello_reset.py Normal file
View File

@ -0,0 +1,110 @@
import numpy as np
import pytest
from lerobot_robot_ufactory.teleoperators.gello_teleop import gello_teleop as gello_module
from lerobot_robot_ufactory.scripts.uf_lerobot_record import _prepare_recording_episode
class FakeDriver:
def __init__(self, positions, follow_commands=True):
self.positions = np.asarray(positions, dtype=float)
self.follow_commands = follow_commands
self.commands = []
def get_joints(self):
return self.positions.copy()
def set_joints(self, positions):
self.commands.append(np.asarray(positions, dtype=float))
if self.follow_commands:
self.positions = self.commands[-1].copy()
class FakeGelloRobot:
def __init__(self, follow_commands=True):
self._driver = FakeDriver([0.0, 0.0, 0.0], follow_commands=follow_commands)
self._joint_signs = np.array([1.0, -1.0, 1.0])
self._joint_offsets = np.array([0.1, 0.2, 0.0])
self.gripper_open_close = (0.0, 1.0)
self._last_pos = object()
self.torque_calls = []
def set_torque_mode(self, enabled):
self.torque_calls.append(enabled)
def make_teleop(robot):
teleop = gello_module.GelloTeleop.__new__(gello_module.GelloTeleop)
teleop._is_connected = True
teleop._teleop_enabled = False
teleop.dof = 2
teleop.gello_agent = type("FakeAgent", (), {"_robot": robot})()
return teleop
def patch_clock(monkeypatch):
clock = [0.0]
monkeypatch.setattr(gello_module.time, "perf_counter", lambda: clock[0])
monkeypatch.setattr(
gello_module.time,
"sleep",
lambda seconds: clock.__setitem__(0, clock[0] + seconds),
)
def test_gello_reset_moves_to_robot_observation_and_disables_torque(monkeypatch):
patch_clock(monkeypatch)
robot = FakeGelloRobot()
teleop = make_teleop(robot)
teleop.reset_to_robot_observation(
{"J1.pos": 0.3, "J2.pos": -0.4, "gripper.pos": 0.5}
)
assert robot.torque_calls == [True, False]
assert np.allclose(robot._driver.positions, [0.4, 0.6, 0.5])
assert robot._last_pos is None
assert teleop._teleop_enabled is False
def test_gello_reset_failure_leaves_torque_off_and_teleop_disabled(monkeypatch):
patch_clock(monkeypatch)
robot = FakeGelloRobot(follow_commands=False)
teleop = make_teleop(robot)
with pytest.raises(RuntimeError, match="did not reach"):
teleop.reset_to_robot_observation(
{"J1.pos": 0.3, "J2.pos": -0.4, "gripper.pos": 0.5}
)
assert robot.torque_calls == [True, False]
assert teleop._teleop_enabled is False
def test_recording_reset_disables_before_robot_and_enables_after_alignment():
calls = []
class FakeRobot:
def reset_to_initial(self):
calls.append("robot_reset")
def get_observation(self):
calls.append("observation")
return {"J1.pos": 0.0}
class FakeTeleop:
def set_teleop_enabled(self, enabled, obs=None):
calls.append(f"teleop_{enabled}")
def reset_to_robot_observation(self, obs):
calls.append("gello_alignment")
_prepare_recording_episode(FakeRobot(), FakeTeleop(), True, False)
assert calls == [
"teleop_False",
"robot_reset",
"observation",
"gello_alignment",
"teleop_True",
]

View File

@ -9,6 +9,7 @@ from lerobot_robot_ufactory.robots.uf_robot.uf_robot_config import UFRobotConfig
from lerobot_robot_ufactory.scripts import uf_lerobot_record as record_module from lerobot_robot_ufactory.scripts import uf_lerobot_record as record_module
from lerobot_robot_ufactory.scripts.uf_lerobot_record import ( from lerobot_robot_ufactory.scripts.uf_lerobot_record import (
_manual_action_from_observation, _manual_action_from_observation,
_prepare_recording_episode,
get_cfg, get_cfg,
) )
@ -20,6 +21,7 @@ class FakeXArm:
self.axis = 6 self.axis = 6
self.error_code = 0 self.error_code = 0
self.mode = 0 self.mode = 0
self.initial_point = [0.0, -30.0, 0.0, 0.0, 0.0, 30.0]
self.calls = [] self.calls = []
def motion_enable(self, **kwargs): def motion_enable(self, **kwargs):
@ -41,6 +43,14 @@ class FakeXArm:
self.calls.append(("set_state", state)) self.calls.append(("set_state", state))
return 0 return 0
def get_initial_point(self):
self.calls.append(("get_initial_point",))
return 0, self.initial_point
def set_servo_angle(self, **kwargs):
self.calls.append(("set_servo_angle", kwargs))
return 0
def get_err_warn_code(self): def get_err_warn_code(self):
return 0, [0, 0] return 0, [0, 0]
@ -72,7 +82,6 @@ def test_manual_mode_robot_enters_teaching_mode_without_sending_actions(monkeypa
robot_dof=6, robot_dof=6,
control_space="joint", control_space="joint",
gripper_type=0, gripper_type=0,
start_joints=(),
manual_mode=True, manual_mode=True,
teach_sensitivity=4, teach_sensitivity=4,
) )
@ -81,10 +90,26 @@ def test_manual_mode_robot_enters_teaching_mode_without_sending_actions(monkeypa
robot.connect() robot.connect()
assert arm.mode == 2 assert arm.mode == 2
assert ("set_teach_sensitivity", 4) in arm.calls assert ("set_teach_sensitivity", 4) in arm.calls
assert robot._initial_point == arm.initial_point
assert not any(call[0] == "set_servo_angle" for call in arm.calls)
robot.reset_to_initial()
reset_calls = [call for call in arm.calls if call[0] == "set_servo_angle"]
assert reset_calls == [
(
"set_servo_angle",
{
"angle": arm.initial_point,
"speed": 60,
"is_radian": False,
"wait": True,
},
)
]
assert arm.mode == 2
action = {"J1.pos": 1.0} action = {"J1.pos": 1.0}
assert robot.send_action(action) is action assert robot.send_action(action) is action
assert not any(call[0] == "set_servo_angle" for call in arm.calls)
observation = robot.get_observation() observation = robot.get_observation()
assert observation["J1.pos"] == 0.0 assert observation["J1.pos"] == 0.0
@ -95,6 +120,42 @@ def test_manual_mode_robot_enters_teaching_mode_without_sending_actions(monkeypa
assert ("disconnect",) in arm.calls assert ("disconnect",) in arm.calls
def test_robot_reset_uses_sdk_initial_point_in_normal_mode(monkeypatch, tmp_path):
from lerobot_robot_ufactory.robots.uf_robot import uf_robot as uf_robot_module
arm = FakeXArm("192.168.1.245")
monkeypatch.setattr(uf_robot_module, "XArmAPI", lambda robot_ip: arm)
monkeypatch.setattr(uf_robot_module.time, "sleep", lambda _: None)
config = UFRobotConfig(
id="test_normal_robot",
calibration_dir=tmp_path,
robot_ip=arm.robot_ip,
robot_dof=6,
control_space="joint",
gripper_type=0,
)
robot = uf_robot_module.UFRobot(config)
assert not hasattr(config, "start_joints")
assert not hasattr(config, "start_tcp_pose")
robot.connect()
reset_calls = [call for call in arm.calls if call[0] == "set_servo_angle"]
assert reset_calls == [
(
"set_servo_angle",
{
"angle": arm.initial_point,
"speed": 60,
"is_radian": False,
"wait": True,
},
)
]
robot.disconnect()
def test_manual_mode_config_rejects_cartesian_control(tmp_path): def test_manual_mode_config_rejects_cartesian_control(tmp_path):
with pytest.raises(ValueError, match="control_space='joint'"): with pytest.raises(ValueError, match="control_space='joint'"):
UFRobotConfig( UFRobotConfig(
@ -201,3 +262,18 @@ def test_manual_record_loop_writes_actual_state_as_action(tmp_path):
dataset.save_episode() dataset.save_episode()
dataset.finalize() dataset.finalize()
def test_manual_recording_episode_resets_before_recording():
class FakeRobot:
def __init__(self):
self.calls = []
def reset_to_initial(self):
self.calls.append("reset_to_initial")
robot = FakeRobot()
_prepare_recording_episode(robot, teleop=None, is_uf_teleop=False, manual_mode=True)
assert robot.calls == ["reset_to_initial"]