| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| from unittest.mock import MagicMock, patch |
|
|
| import numpy as np |
| import pytest |
|
|
| pytest.importorskip("reachy2_sdk") |
|
|
| from lerobot.robots.reachy2 import ( |
| REACHY2_ANTENNAS_JOINTS, |
| REACHY2_L_ARM_JOINTS, |
| REACHY2_NECK_JOINTS, |
| REACHY2_R_ARM_JOINTS, |
| REACHY2_VEL, |
| Reachy2Robot, |
| Reachy2RobotConfig, |
| ) |
|
|
| |
| REACHY2_JOINTS = { |
| **REACHY2_NECK_JOINTS, |
| **REACHY2_ANTENNAS_JOINTS, |
| **REACHY2_R_ARM_JOINTS, |
| **REACHY2_L_ARM_JOINTS, |
| } |
|
|
| PARAMS = [ |
| {}, |
| {"with_mobile_base": False}, |
| {"with_mobile_base": False, "with_l_arm": False, "with_antennas": False}, |
| {"with_r_arm": False, "with_neck": False, "with_antennas": False}, |
| {"use_external_commands": True, "disable_torque_on_disconnect": True}, |
| {"use_external_commands": True, "with_mobile_base": False, "with_neck": False}, |
| {"disable_torque_on_disconnect": False}, |
| {"max_relative_target": 5}, |
| {"with_right_teleop_camera": False}, |
| {"with_left_teleop_camera": False, "with_right_teleop_camera": False}, |
| {"with_left_teleop_camera": False, "with_torso_camera": True}, |
| ] |
|
|
|
|
| def _make_reachy2_sdk_mock(): |
| class JointSpy: |
| __slots__ = ( |
| "present_position", |
| "_goal_position", |
| "_on_set", |
| ) |
|
|
| def __init__(self, present_position=0.0, on_set=None): |
| self.present_position = present_position |
| self._goal_position = present_position |
| self._on_set = on_set |
|
|
| @property |
| def goal_position(self): |
| return self._goal_position |
|
|
| @goal_position.setter |
| def goal_position(self, v): |
| self._goal_position = v |
| if self._on_set: |
| self._on_set() |
|
|
| r = MagicMock(name="ReachySDKMock") |
| r.is_connected.return_value = True |
|
|
| def _connect(): |
| r.is_connected.return_value = True |
|
|
| def _disconnect(): |
| r.is_connected.return_value = False |
|
|
| |
| r._goal_position_set_total = 0 |
|
|
| def _on_any_goal_set(): |
| r._goal_position_set_total += 1 |
|
|
| |
| joints = { |
| k: JointSpy( |
| present_position=float(i), |
| on_set=_on_any_goal_set, |
| ) |
| for i, k in enumerate(REACHY2_JOINTS.values()) |
| } |
| r.joints = joints |
|
|
| |
| r.mobile_base = MagicMock() |
| r.mobile_base.odometry = { |
| "x": 0.1, |
| "y": -0.2, |
| "theta": 21.3, |
| "vx": 0.001, |
| "vy": 0.002, |
| "vtheta": 0.0, |
| } |
|
|
| r.connect = MagicMock(side_effect=_connect) |
| r.disconnect = MagicMock(side_effect=_disconnect) |
|
|
| |
| r.turn_on = MagicMock() |
| r.reset_default_limits = MagicMock() |
| r.send_goal_positions = MagicMock() |
| r.turn_off_smoothly = MagicMock() |
| r.mobile_base.set_goal_speed = MagicMock() |
| r.mobile_base.send_speed_command = MagicMock() |
|
|
| return r |
|
|
|
|
| def _make_reachy2_camera_mock(*args, **kwargs): |
| cfg = args[0] if args else kwargs.get("config") |
| name = getattr(cfg, "name", kwargs.get("name", "cam")) |
| image_type = getattr(cfg, "image_type", kwargs.get("image_type", "cam")) |
| width = getattr(cfg, "width", kwargs.get("width", 640)) |
| height = getattr(cfg, "height", kwargs.get("height", 480)) |
|
|
| cam = MagicMock(name=f"Reachy2CameraMock:{name}") |
| cam.name = name |
| cam.image_type = image_type |
| cam.width = width |
| cam.height = height |
| cam.connect = MagicMock() |
| cam.disconnect = MagicMock() |
| cam.async_read = MagicMock(side_effect=lambda: np.zeros((height, width, 3), dtype=np.uint8)) |
| cam.read_latest = MagicMock(side_effect=lambda: np.zeros((height, width, 3), dtype=np.uint8)) |
| return cam |
|
|
|
|
| @pytest.fixture(params=PARAMS, ids=lambda p: "default" if not p else ",".join(p.keys())) |
| def reachy2(request): |
| with ( |
| patch( |
| "lerobot.robots.reachy2.robot_reachy2.ReachySDK", |
| side_effect=lambda *a, **k: _make_reachy2_sdk_mock(), |
| ), |
| patch( |
| "lerobot.cameras.reachy2_camera.reachy2_camera.Reachy2Camera", |
| side_effect=_make_reachy2_camera_mock, |
| ), |
| ): |
| overrides = request.param |
| cfg = Reachy2RobotConfig(ip_address="192.168.0.200", **overrides) |
| robot = Reachy2Robot(cfg) |
| yield robot |
| if robot.is_connected: |
| robot.disconnect() |
|
|
|
|
| def test_connect_disconnect(reachy2): |
| assert not reachy2.is_connected |
|
|
| reachy2.connect() |
| assert reachy2.is_connected |
|
|
| reachy2.reachy.turn_on.assert_called_once() |
| reachy2.reachy.reset_default_limits.assert_called_once() |
|
|
| reachy2.disconnect() |
| assert not reachy2.is_connected |
|
|
| if reachy2.config.disable_torque_on_disconnect: |
| reachy2.reachy.turn_off_smoothly.assert_called_once() |
| else: |
| reachy2.reachy.turn_off_smoothly.assert_not_called() |
| reachy2.reachy.disconnect.assert_called_once() |
|
|
|
|
| def test_get_joints_dict(reachy2): |
| reachy2.connect() |
|
|
| if reachy2.config.with_neck: |
| assert "neck_yaw.pos" in reachy2.joints_dict |
| assert "neck_pitch.pos" in reachy2.joints_dict |
| assert "neck_roll.pos" in reachy2.joints_dict |
| else: |
| assert "neck_yaw.pos" not in reachy2.joints_dict |
| assert "neck_pitch.pos" not in reachy2.joints_dict |
| assert "neck_roll.pos" not in reachy2.joints_dict |
|
|
| if reachy2.config.with_antennas: |
| assert "l_antenna.pos" in reachy2.joints_dict |
| assert "r_antenna.pos" in reachy2.joints_dict |
| else: |
| assert "l_antenna.pos" not in reachy2.joints_dict |
| assert "r_antenna.pos" not in reachy2.joints_dict |
|
|
| if reachy2.config.with_r_arm: |
| assert "r_shoulder_pitch.pos" in reachy2.joints_dict |
| assert "r_shoulder_roll.pos" in reachy2.joints_dict |
| assert "r_elbow_yaw.pos" in reachy2.joints_dict |
| assert "r_elbow_pitch.pos" in reachy2.joints_dict |
| assert "r_wrist_roll.pos" in reachy2.joints_dict |
| assert "r_wrist_pitch.pos" in reachy2.joints_dict |
| assert "r_wrist_yaw.pos" in reachy2.joints_dict |
| assert "r_gripper.pos" in reachy2.joints_dict |
| else: |
| assert "r_shoulder_pitch.pos" not in reachy2.joints_dict |
| assert "r_shoulder_roll.pos" not in reachy2.joints_dict |
| assert "r_elbow_yaw.pos" not in reachy2.joints_dict |
| assert "r_elbow_pitch.pos" not in reachy2.joints_dict |
| assert "r_wrist_roll.pos" not in reachy2.joints_dict |
| assert "r_wrist_pitch.pos" not in reachy2.joints_dict |
| assert "r_wrist_yaw.pos" not in reachy2.joints_dict |
| assert "r_gripper.pos" not in reachy2.joints_dict |
|
|
| if reachy2.config.with_l_arm: |
| assert "l_shoulder_pitch.pos" in reachy2.joints_dict |
| assert "l_shoulder_roll.pos" in reachy2.joints_dict |
| assert "l_elbow_yaw.pos" in reachy2.joints_dict |
| assert "l_elbow_pitch.pos" in reachy2.joints_dict |
| assert "l_wrist_roll.pos" in reachy2.joints_dict |
| assert "l_wrist_pitch.pos" in reachy2.joints_dict |
| assert "l_wrist_yaw.pos" in reachy2.joints_dict |
| assert "l_gripper.pos" in reachy2.joints_dict |
| else: |
| assert "l_shoulder_pitch.pos" not in reachy2.joints_dict |
| assert "l_shoulder_roll.pos" not in reachy2.joints_dict |
| assert "l_elbow_yaw.pos" not in reachy2.joints_dict |
| assert "l_elbow_pitch.pos" not in reachy2.joints_dict |
| assert "l_wrist_roll.pos" not in reachy2.joints_dict |
| assert "l_wrist_pitch.pos" not in reachy2.joints_dict |
| assert "l_wrist_yaw.pos" not in reachy2.joints_dict |
| assert "l_gripper.pos" not in reachy2.joints_dict |
|
|
|
|
| def test_get_observation(reachy2): |
| reachy2.connect() |
| obs = reachy2.get_observation() |
|
|
| expected_keys = set(reachy2.joints_dict) |
| expected_keys.update(f"{v}" for v in REACHY2_VEL if reachy2.config.with_mobile_base) |
| expected_keys.update(reachy2.cameras.keys()) |
| assert set(obs.keys()) == expected_keys |
|
|
| for motor in reachy2.joints_dict: |
| assert obs[motor] == reachy2.reachy.joints[REACHY2_JOINTS[motor]].present_position |
| if reachy2.config.with_mobile_base: |
| for vel in REACHY2_VEL: |
| assert obs[vel] == reachy2.reachy.mobile_base.odometry[REACHY2_VEL[vel]] |
| if reachy2.config.with_left_teleop_camera: |
| assert obs["teleop_left"].shape == ( |
| reachy2.config.cameras["teleop_left"].height, |
| reachy2.config.cameras["teleop_left"].width, |
| 3, |
| ) |
| if reachy2.config.with_right_teleop_camera: |
| assert obs["teleop_right"].shape == ( |
| reachy2.config.cameras["teleop_right"].height, |
| reachy2.config.cameras["teleop_right"].width, |
| 3, |
| ) |
| if reachy2.config.with_torso_camera: |
| assert obs["torso_rgb"].shape == ( |
| reachy2.config.cameras["torso_rgb"].height, |
| reachy2.config.cameras["torso_rgb"].width, |
| 3, |
| ) |
|
|
|
|
| def test_send_action(reachy2): |
| reachy2.connect() |
|
|
| action = {k: i * 10.0 for i, k in enumerate(reachy2.joints_dict.keys(), start=1)} |
| if reachy2.config.with_mobile_base: |
| action.update({k: i * 0.1 for i, k in enumerate(REACHY2_VEL.keys(), start=1)}) |
|
|
| previous_present_position = { |
| k: reachy2.reachy.joints[REACHY2_JOINTS[k]].present_position for k in reachy2.joints_dict |
| } |
| returned = reachy2.send_action(action) |
|
|
| if reachy2.config.max_relative_target is None: |
| assert returned == action |
|
|
| assert reachy2.reachy._goal_position_set_total == len(reachy2.joints_dict) |
| for motor in reachy2.joints_dict: |
| expected_pos = action[motor] |
| real_pos = reachy2.reachy.joints[REACHY2_JOINTS[motor]].goal_position |
| if reachy2.config.max_relative_target is None: |
| assert real_pos == expected_pos |
| else: |
| assert real_pos == previous_present_position[motor] + np.sign(expected_pos) * min( |
| abs(expected_pos - real_pos), reachy2.config.max_relative_target |
| ) |
|
|
| if reachy2.config.with_mobile_base: |
| goal_speed = [i * 0.1 for i, _ in enumerate(REACHY2_VEL.keys(), start=1)] |
| reachy2.reachy.mobile_base.set_goal_speed.assert_called_once_with(*goal_speed) |
|
|
| if reachy2.config.use_external_commands: |
| reachy2.reachy.send_goal_positions.assert_not_called() |
| if reachy2.config.with_mobile_base: |
| reachy2.reachy.mobile_base.send_speed_command.assert_not_called() |
| else: |
| reachy2.reachy.send_goal_positions.assert_called_once() |
| if reachy2.config.with_mobile_base: |
| reachy2.reachy.mobile_base.send_speed_command.assert_called_once() |
|
|
|
|
| def test_no_part_declared(): |
| with pytest.raises(ValueError): |
| _ = Reachy2RobotConfig( |
| ip_address="192.168.0.200", |
| with_mobile_base=False, |
| with_l_arm=False, |
| with_r_arm=False, |
| with_neck=False, |
| with_antennas=False, |
| ) |
|
|