Add gello and robot reset pose logic.
This commit is contained in:
parent
b51dfa128a
commit
2f7077fcbf
@ -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)
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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()
|
||||||
|
|
||||||
|
if self.config.manual_mode:
|
||||||
self.configure()
|
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":
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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 **********")
|
||||||
|
if is_uf_teleop:
|
||||||
|
print('⌨ [ESC] Exit [Space] Reset / Start [←] Reset')
|
||||||
|
else:
|
||||||
print('⌨ [ESC] Exit [Space] Start [←] Reset')
|
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')
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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.")
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -111,6 +111,7 @@ 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:
|
||||||
|
if self._last_action is not None:
|
||||||
obs = self._last_action
|
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"]]
|
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:
|
if self.config.use_gripper:
|
||||||
|
|||||||
110
tests/test_gello_reset.py
Normal file
110
tests/test_gello_reset.py
Normal 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",
|
||||||
|
]
|
||||||
@ -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"]
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user