From 474210b7498e2033e076f237d152d5338d3ee0c4 Mon Sep 17 00:00:00 2001 From: Steven Palma Date: Thu, 8 Jan 2026 16:51:50 +0100 Subject: [PATCH 1/7] feat(robots): consolidates bi SO setups --- src/lerobot/async_inference/robot_client.py | 2 +- .../bi_so100_follower/bi_so100_follower.py | 162 ------------------ .../config_bi_so100_follower.py | 39 ----- .../bi_so_follower}/__init__.py | 4 +- .../robots/bi_so_follower/bi_so_follower.py | 130 ++++++++++++++ .../bi_so_follower/config_bi_so_follower.py | 29 ++++ src/lerobot/robots/utils.py | 6 +- src/lerobot/scripts/lerobot_record.py | 4 +- src/lerobot/scripts/lerobot_replay.py | 2 +- src/lerobot/scripts/lerobot_teleoperate.py | 4 +- .../bi_so_leader}/__init__.py | 4 +- .../bi_so_leader.py} | 54 ++---- .../config_bi_so_leader.py} | 11 +- src/lerobot/teleoperators/utils.py | 6 +- 14 files changed, 199 insertions(+), 258 deletions(-) delete mode 100644 src/lerobot/robots/bi_so100_follower/bi_so100_follower.py delete mode 100644 src/lerobot/robots/bi_so100_follower/config_bi_so100_follower.py rename src/lerobot/{teleoperators/bi_so100_leader => robots/bi_so_follower}/__init__.py (86%) create mode 100644 src/lerobot/robots/bi_so_follower/bi_so_follower.py create mode 100644 src/lerobot/robots/bi_so_follower/config_bi_so_follower.py rename src/lerobot/{robots/bi_so100_follower => teleoperators/bi_so_leader}/__init__.py (85%) rename src/lerobot/teleoperators/{bi_so100_leader/bi_so100_leader.py => bi_so_leader/bi_so_leader.py} (56%) rename src/lerobot/teleoperators/{bi_so100_leader/config_bi_so100_leader.py => bi_so_leader/config_bi_so_leader.py} (71%) diff --git a/src/lerobot/async_inference/robot_client.py b/src/lerobot/async_inference/robot_client.py index c3668d40bd4..eea5585b03d 100644 --- a/src/lerobot/async_inference/robot_client.py +++ b/src/lerobot/async_inference/robot_client.py @@ -51,7 +51,7 @@ from lerobot.robots import ( # noqa: F401 Robot, RobotConfig, - bi_so100_follower, + bi_so_follower, koch_follower, make_robot_from_config, omx_follower, diff --git a/src/lerobot/robots/bi_so100_follower/bi_so100_follower.py b/src/lerobot/robots/bi_so100_follower/bi_so100_follower.py deleted file mode 100644 index 87a7edcc5de..00000000000 --- a/src/lerobot/robots/bi_so100_follower/bi_so100_follower.py +++ /dev/null @@ -1,162 +0,0 @@ -#!/usr/bin/env python - -# Copyright 2025 The HuggingFace Inc. team. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import logging -import time -from functools import cached_property -from typing import Any - -from lerobot.cameras.utils import make_cameras_from_configs -from lerobot.robots.so_follower import SO100Follower, SO100FollowerConfig - -from ..robot import Robot -from .config_bi_so100_follower import BiSO100FollowerConfig - -logger = logging.getLogger(__name__) - - -class BiSO100Follower(Robot): - """ - [Bimanual SO-100 Follower Arms](https://github.com/TheRobotStudio/SO-ARM100) designed by TheRobotStudio - This bimanual robot can also be easily adapted to use SO-101 follower arms, just replace the SO100Follower class with SO101Follower and SO100FollowerConfig with SO101FollowerConfig. - """ - - config_class = BiSO100FollowerConfig - name = "bi_so100_follower" - - def __init__(self, config: BiSO100FollowerConfig): - super().__init__(config) - self.config = config - - left_arm_config = SO100FollowerConfig( - id=f"{config.id}_left" if config.id else None, - calibration_dir=config.calibration_dir, - port=config.left_arm_port, - disable_torque_on_disconnect=config.left_arm_disable_torque_on_disconnect, - max_relative_target=config.left_arm_max_relative_target, - use_degrees=config.left_arm_use_degrees, - cameras={}, - ) - - right_arm_config = SO100FollowerConfig( - id=f"{config.id}_right" if config.id else None, - calibration_dir=config.calibration_dir, - port=config.right_arm_port, - disable_torque_on_disconnect=config.right_arm_disable_torque_on_disconnect, - max_relative_target=config.right_arm_max_relative_target, - use_degrees=config.right_arm_use_degrees, - cameras={}, - ) - - self.left_arm = SO100Follower(left_arm_config) - self.right_arm = SO100Follower(right_arm_config) - self.cameras = make_cameras_from_configs(config.cameras) - - @property - def _motors_ft(self) -> dict[str, type]: - return {f"left_{motor}.pos": float for motor in self.left_arm.bus.motors} | { - f"right_{motor}.pos": float for motor in self.right_arm.bus.motors - } - - @property - def _cameras_ft(self) -> dict[str, tuple]: - return { - cam: (self.config.cameras[cam].height, self.config.cameras[cam].width, 3) for cam in self.cameras - } - - @cached_property - def observation_features(self) -> dict[str, type | tuple]: - return {**self._motors_ft, **self._cameras_ft} - - @cached_property - def action_features(self) -> dict[str, type]: - return self._motors_ft - - @property - def is_connected(self) -> bool: - return ( - self.left_arm.bus.is_connected - and self.right_arm.bus.is_connected - and all(cam.is_connected for cam in self.cameras.values()) - ) - - def connect(self, calibrate: bool = True) -> None: - self.left_arm.connect(calibrate) - self.right_arm.connect(calibrate) - - for cam in self.cameras.values(): - cam.connect() - - @property - def is_calibrated(self) -> bool: - return self.left_arm.is_calibrated and self.right_arm.is_calibrated - - def calibrate(self) -> None: - self.left_arm.calibrate() - self.right_arm.calibrate() - - def configure(self) -> None: - self.left_arm.configure() - self.right_arm.configure() - - def setup_motors(self) -> None: - self.left_arm.setup_motors() - self.right_arm.setup_motors() - - def get_observation(self) -> dict[str, Any]: - obs_dict = {} - - # Add "left_" prefix - left_obs = self.left_arm.get_observation() - obs_dict.update({f"left_{key}": value for key, value in left_obs.items()}) - - # Add "right_" prefix - right_obs = self.right_arm.get_observation() - obs_dict.update({f"right_{key}": value for key, value in right_obs.items()}) - - for cam_key, cam in self.cameras.items(): - start = time.perf_counter() - obs_dict[cam_key] = cam.async_read() - dt_ms = (time.perf_counter() - start) * 1e3 - logger.debug(f"{self} read {cam_key}: {dt_ms:.1f}ms") - - return obs_dict - - def send_action(self, action: dict[str, Any]) -> dict[str, Any]: - # Remove "left_" prefix - left_action = { - key.removeprefix("left_"): value for key, value in action.items() if key.startswith("left_") - } - # Remove "right_" prefix - right_action = { - key.removeprefix("right_"): value for key, value in action.items() if key.startswith("right_") - } - - send_action_left = self.left_arm.send_action(left_action) - send_action_right = self.right_arm.send_action(right_action) - - # Add prefixes back - prefixed_send_action_left = {f"left_{key}": value for key, value in send_action_left.items()} - prefixed_send_action_right = {f"right_{key}": value for key, value in send_action_right.items()} - - return {**prefixed_send_action_left, **prefixed_send_action_right} - - def disconnect(self): - self.left_arm.disconnect() - self.right_arm.disconnect() - - for cam in self.cameras.values(): - cam.disconnect() diff --git a/src/lerobot/robots/bi_so100_follower/config_bi_so100_follower.py b/src/lerobot/robots/bi_so100_follower/config_bi_so100_follower.py deleted file mode 100644 index 5806d74159f..00000000000 --- a/src/lerobot/robots/bi_so100_follower/config_bi_so100_follower.py +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env python - -# Copyright 2025 The HuggingFace Inc. team. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dataclasses import dataclass, field - -from lerobot.cameras import CameraConfig - -from ..config import RobotConfig - - -@RobotConfig.register_subclass("bi_so100_follower") -@dataclass -class BiSO100FollowerConfig(RobotConfig): - left_arm_port: str - right_arm_port: str - - # Optional - left_arm_disable_torque_on_disconnect: bool = True - left_arm_max_relative_target: float | dict[str, float] | None = None - left_arm_use_degrees: bool = False - right_arm_disable_torque_on_disconnect: bool = True - right_arm_max_relative_target: float | dict[str, float] | None = None - right_arm_use_degrees: bool = False - - # cameras (shared between both arms) - cameras: dict[str, CameraConfig] = field(default_factory=dict) diff --git a/src/lerobot/teleoperators/bi_so100_leader/__init__.py b/src/lerobot/robots/bi_so_follower/__init__.py similarity index 86% rename from src/lerobot/teleoperators/bi_so100_leader/__init__.py rename to src/lerobot/robots/bi_so_follower/__init__.py index 34313a61e6b..d93f7e30a22 100644 --- a/src/lerobot/teleoperators/bi_so100_leader/__init__.py +++ b/src/lerobot/robots/bi_so_follower/__init__.py @@ -14,5 +14,5 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .bi_so100_leader import BiSO100Leader -from .config_bi_so100_leader import BiSO100LeaderConfig +from .bi_so_follower import BiSOFollower +from .config_bi_so_follower import BiSOFollowerConfig diff --git a/src/lerobot/robots/bi_so_follower/bi_so_follower.py b/src/lerobot/robots/bi_so_follower/bi_so_follower.py new file mode 100644 index 00000000000..d107d255d03 --- /dev/null +++ b/src/lerobot/robots/bi_so_follower/bi_so_follower.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python + +# Copyright 2025 The HuggingFace Inc. team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +from functools import cached_property +from typing import Any + +from lerobot.robots.so_follower import SOFollowerBase + +from ..robot import Robot +from .config_bi_so_follower import BiSOFollowerConfig + +logger = logging.getLogger(__name__) + + +class BiSOFollower(Robot): + """ + [Bimanual SO Follower Arms](https://github.com/TheRobotStudio/SO-ARM100) designed by TheRobotStudio + """ + + config_class = BiSOFollowerConfig + name = "bi_so_follower" + + def __init__(self, config: BiSOFollowerConfig): + super().__init__(config) + self.config = config + + self.left_arm = SOFollowerBase(self.config.left_arm_config) + self.right_arm = SOFollowerBase(self.config.right_arm_config) + + @property + def _motors_ft(self) -> dict[str, type]: + left_arm_motors_ft = self.left_arm._motors_ft + right_arm_motors_ft = self.right_arm._motors_ft + + return { + **{f"left_{k}": v for k, v in left_arm_motors_ft.items()}, + **{f"right_{k}": v for k, v in right_arm_motors_ft.items()}, + } + + @property + def _cameras_ft(self) -> dict[str, tuple]: + left_arm_cameras_ft = self.left_arm._cameras_ft + right_arm_cameras_ft = self.right_arm._cameras_ft + + return { + **{f"left_{k}": v for k, v in left_arm_cameras_ft.items()}, + **{f"right_{k}": v for k, v in right_arm_cameras_ft.items()}, + } + + @cached_property + def observation_features(self) -> dict[str, type | tuple]: + return {**self._motors_ft, **self._cameras_ft} + + @cached_property + def action_features(self) -> dict[str, type]: + return self._motors_ft + + @property + def is_connected(self) -> bool: + return self.left_arm.is_connected and self.right_arm.is_connected + + def connect(self, calibrate: bool = True) -> None: + self.left_arm.connect(calibrate) + self.right_arm.connect(calibrate) + + @property + def is_calibrated(self) -> bool: + return self.left_arm.is_calibrated and self.right_arm.is_calibrated + + def calibrate(self) -> None: + self.left_arm.calibrate() + self.right_arm.calibrate() + + def configure(self) -> None: + self.left_arm.configure() + self.right_arm.configure() + + def setup_motors(self) -> None: + self.left_arm.setup_motors() + self.right_arm.setup_motors() + + def get_observation(self) -> dict[str, Any]: + obs_dict = {} + + # Add "left_" prefix + left_obs = self.left_arm.get_observation() + obs_dict.update({f"left_{key}": value for key, value in left_obs.items()}) + + # Add "right_" prefix + right_obs = self.right_arm.get_observation() + obs_dict.update({f"right_{key}": value for key, value in right_obs.items()}) + + return obs_dict + + def send_action(self, action: dict[str, Any]) -> dict[str, Any]: + # Remove "left_" prefix + left_action = { + key.removeprefix("left_"): value for key, value in action.items() if key.startswith("left_") + } + # Remove "right_" prefix + right_action = { + key.removeprefix("right_"): value for key, value in action.items() if key.startswith("right_") + } + + sent_action_left = self.left_arm.send_action(left_action) + sent_action_right = self.right_arm.send_action(right_action) + + # Add prefixes back + prefixed_sent_action_left = {f"left_{key}": value for key, value in sent_action_left.items()} + prefixed_sent_action_right = {f"right_{key}": value for key, value in sent_action_right.items()} + + return {**prefixed_sent_action_left, **prefixed_sent_action_right} + + def disconnect(self): + self.left_arm.disconnect() + self.right_arm.disconnect() diff --git a/src/lerobot/robots/bi_so_follower/config_bi_so_follower.py b/src/lerobot/robots/bi_so_follower/config_bi_so_follower.py new file mode 100644 index 00000000000..0f8efb65409 --- /dev/null +++ b/src/lerobot/robots/bi_so_follower/config_bi_so_follower.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python + +# Copyright 2025 The HuggingFace Inc. team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from dataclasses import dataclass + +from ..config import RobotConfig +from ..so_follower import SOFollowerConfigBase + + +@RobotConfig.register_subclass("bi_so_follower") +@dataclass +class BiSOFollowerConfig(RobotConfig): + """Configuration class for Bi SO Follower robots.""" + + left_arm_config: SOFollowerConfigBase + right_arm_config: SOFollowerConfigBase diff --git a/src/lerobot/robots/utils.py b/src/lerobot/robots/utils.py index ad6cc3da1e3..27abaaa8690 100644 --- a/src/lerobot/robots/utils.py +++ b/src/lerobot/robots/utils.py @@ -52,10 +52,10 @@ def make_robot_from_config(config: RobotConfig) -> Robot: from .hope_jr import HopeJrArm return HopeJrArm(config) - elif config.type == "bi_so100_follower": - from .bi_so100_follower import BiSO100Follower + elif config.type == "bi_so_follower": + from .bi_so_follower import BiSOFollower - return BiSO100Follower(config) + return BiSOFollower(config) elif config.type == "reachy2": from .reachy2 import Reachy2Robot diff --git a/src/lerobot/scripts/lerobot_record.py b/src/lerobot/scripts/lerobot_record.py index 5d2945e67c3..511fa6c1c23 100644 --- a/src/lerobot/scripts/lerobot_record.py +++ b/src/lerobot/scripts/lerobot_record.py @@ -92,7 +92,7 @@ from lerobot.robots import ( # noqa: F401 Robot, RobotConfig, - bi_so100_follower, + bi_so_follower, earthrover_mini_plus, hope_jr, koch_follower, @@ -103,7 +103,7 @@ from lerobot.teleoperators import ( # noqa: F401 Teleoperator, TeleoperatorConfig, - bi_so100_leader, + bi_so_leader, homunculus, koch_leader, make_teleoperator_from_config, diff --git a/src/lerobot/scripts/lerobot_replay.py b/src/lerobot/scripts/lerobot_replay.py index af7c6336513..35e7b693d91 100644 --- a/src/lerobot/scripts/lerobot_replay.py +++ b/src/lerobot/scripts/lerobot_replay.py @@ -53,7 +53,7 @@ from lerobot.robots import ( # noqa: F401 Robot, RobotConfig, - bi_so100_follower, + bi_so_follower, earthrover_mini_plus, hope_jr, koch_follower, diff --git a/src/lerobot/scripts/lerobot_teleoperate.py b/src/lerobot/scripts/lerobot_teleoperate.py index 2e0724574bf..f743b1747e7 100644 --- a/src/lerobot/scripts/lerobot_teleoperate.py +++ b/src/lerobot/scripts/lerobot_teleoperate.py @@ -70,7 +70,7 @@ from lerobot.robots import ( # noqa: F401 Robot, RobotConfig, - bi_so100_follower, + bi_so_follower, earthrover_mini_plus, hope_jr, koch_follower, @@ -81,7 +81,7 @@ from lerobot.teleoperators import ( # noqa: F401 Teleoperator, TeleoperatorConfig, - bi_so100_leader, + bi_so_leader, gamepad, homunculus, keyboard, diff --git a/src/lerobot/robots/bi_so100_follower/__init__.py b/src/lerobot/teleoperators/bi_so_leader/__init__.py similarity index 85% rename from src/lerobot/robots/bi_so100_follower/__init__.py rename to src/lerobot/teleoperators/bi_so_leader/__init__.py index 90f56516b69..09932aaa520 100644 --- a/src/lerobot/robots/bi_so100_follower/__init__.py +++ b/src/lerobot/teleoperators/bi_so_leader/__init__.py @@ -14,5 +14,5 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .bi_so100_follower import BiSO100Follower -from .config_bi_so100_follower import BiSO100FollowerConfig +from .bi_so_leader import BiSOLeader +from .config_bi_so_leader import BiSOLeaderConfig diff --git a/src/lerobot/teleoperators/bi_so100_leader/bi_so100_leader.py b/src/lerobot/teleoperators/bi_so_leader/bi_so_leader.py similarity index 56% rename from src/lerobot/teleoperators/bi_so100_leader/bi_so100_leader.py rename to src/lerobot/teleoperators/bi_so_leader/bi_so_leader.py index 93f66eb2e91..bf1d5ed1111 100644 --- a/src/lerobot/teleoperators/bi_so100_leader/bi_so100_leader.py +++ b/src/lerobot/teleoperators/bi_so_leader/bi_so_leader.py @@ -17,46 +17,37 @@ import logging from functools import cached_property -from lerobot.teleoperators.so_leader import SO100Leader, SO100LeaderConfig +from lerobot.teleoperators.so_leader import SOLeaderBase from ..teleoperator import Teleoperator -from .config_bi_so100_leader import BiSO100LeaderConfig +from .config_bi_so_leader import BiSOLeaderConfig logger = logging.getLogger(__name__) -class BiSO100Leader(Teleoperator): +class BiSOLeader(Teleoperator): """ - [Bimanual SO-100 Leader Arms](https://github.com/TheRobotStudio/SO-ARM100) designed by TheRobotStudio - This bimanual leader arm can also be easily adapted to use SO-101 leader arms, just replace the SO100Leader class with SO101Leader and SO100LeaderConfig with SO101LeaderConfig. + [Bimanual SO Leader Arms](https://github.com/TheRobotStudio/SO-ARM100) designed by TheRobotStudio """ - config_class = BiSO100LeaderConfig - name = "bi_so100_leader" + config_class = BiSOLeaderConfig + name = "bi_so_leader" - def __init__(self, config: BiSO100LeaderConfig): + def __init__(self, config: BiSOLeaderConfig): super().__init__(config) self.config = config - left_arm_config = SO100LeaderConfig( - id=f"{config.id}_left" if config.id else None, - calibration_dir=config.calibration_dir, - port=config.left_arm_port, - ) - - right_arm_config = SO100LeaderConfig( - id=f"{config.id}_right" if config.id else None, - calibration_dir=config.calibration_dir, - port=config.right_arm_port, - ) - - self.left_arm = SO100Leader(left_arm_config) - self.right_arm = SO100Leader(right_arm_config) + self.left_arm = SOLeaderBase(self.config.left_arm_config) + self.right_arm = SOLeaderBase(self.config.right_arm_config) @cached_property def action_features(self) -> dict[str, type]: - return {f"left_{motor}.pos": float for motor in self.left_arm.bus.motors} | { - f"right_{motor}.pos": float for motor in self.right_arm.bus.motors + left_arm_features = self.left_arm.action_features + right_arm_features = self.right_arm.action_features + + return { + **{f"left_{k}": v for k, v in left_arm_features.items()}, + **{f"right_{k}": v for k, v in right_arm_features.items()}, } @cached_property @@ -101,19 +92,8 @@ def get_action(self) -> dict[str, float]: return action_dict def send_feedback(self, feedback: dict[str, float]) -> None: - # Remove "left_" prefix - left_feedback = { - key.removeprefix("left_"): value for key, value in feedback.items() if key.startswith("left_") - } - # Remove "right_" prefix - right_feedback = { - key.removeprefix("right_"): value for key, value in feedback.items() if key.startswith("right_") - } - - if left_feedback: - self.left_arm.send_feedback(left_feedback) - if right_feedback: - self.right_arm.send_feedback(right_feedback) + # TODO: Implement force feedback + raise NotImplementedError def disconnect(self) -> None: self.left_arm.disconnect() diff --git a/src/lerobot/teleoperators/bi_so100_leader/config_bi_so100_leader.py b/src/lerobot/teleoperators/bi_so_leader/config_bi_so_leader.py similarity index 71% rename from src/lerobot/teleoperators/bi_so100_leader/config_bi_so100_leader.py rename to src/lerobot/teleoperators/bi_so_leader/config_bi_so_leader.py index 117e099131e..d1d24fb34b3 100644 --- a/src/lerobot/teleoperators/bi_so100_leader/config_bi_so100_leader.py +++ b/src/lerobot/teleoperators/bi_so_leader/config_bi_so_leader.py @@ -17,10 +17,13 @@ from dataclasses import dataclass from ..config import TeleoperatorConfig +from ..so_leader import SOLeaderConfigBase -@TeleoperatorConfig.register_subclass("bi_so100_leader") +@TeleoperatorConfig.register_subclass("bi_so_leader") @dataclass -class BiSO100LeaderConfig(TeleoperatorConfig): - left_arm_port: str - right_arm_port: str +class BiSOLeaderConfig(TeleoperatorConfig): + """Configuration class for Bi SO Leader teleoperators.""" + + left_arm_config: SOLeaderConfigBase + right_arm_config: SOLeaderConfigBase diff --git a/src/lerobot/teleoperators/utils.py b/src/lerobot/teleoperators/utils.py index 74e43ec9509..eec2f119c63 100644 --- a/src/lerobot/teleoperators/utils.py +++ b/src/lerobot/teleoperators/utils.py @@ -73,10 +73,10 @@ def make_teleoperator_from_config(config: TeleoperatorConfig) -> Teleoperator: from .homunculus import HomunculusArm return HomunculusArm(config) - elif config.type == "bi_so100_leader": - from .bi_so100_leader import BiSO100Leader + elif config.type == "bi_so_leader": + from .bi_so_leader import BiSOLeader - return BiSO100Leader(config) + return BiSOLeader(config) elif config.type == "reachy2_teleoperator": from .reachy2_teleoperator import Reachy2Teleoperator From 16e22a5cf79e4ab1056009ba8ef8a797b65ae6eb Mon Sep 17 00:00:00 2001 From: Steven Palma Date: Fri, 9 Jan 2026 17:21:16 +0100 Subject: [PATCH 2/7] fix(robots): solve circular dependecy --- docs/source/groot.mdx | 2 +- examples/so100_to_so100_EE/record.py | 3 +- examples/so100_to_so100_EE/teleoperate.py | 3 +- src/lerobot/async_inference/constants.py | 2 +- src/lerobot/async_inference/robot_client.py | 1 - src/lerobot/robots/bi_so_follower/__init__.py | 18 ----------- src/lerobot/robots/so_follower/__init__.py | 5 ++-- .../bi_so_follower/bi_so_follower.py | 30 +++++++++++++++---- .../bi_so_follower/config_bi_so_follower.py | 4 +-- .../so100_follower/config_so100_follower.py | 4 +-- .../so101_follower/config_so101_follower.py | 4 +-- .../robots/so_follower/so_follower_base.py | 4 +-- .../so_follower/so_follower_config_base.py | 7 ++++- src/lerobot/robots/utils.py | 2 +- src/lerobot/scripts/lerobot_record.py | 6 ++-- src/lerobot/scripts/lerobot_replay.py | 3 +- src/lerobot/scripts/lerobot_teleoperate.py | 6 ++-- .../teleoperators/bi_so_leader/__init__.py | 18 ----------- .../teleoperators/so_leader/__init__.py | 4 +-- .../bi_so_leader/bi_so_leader.py | 22 ++++++++++---- .../bi_so_leader/config_bi_so_leader.py | 4 +-- .../so100_leader/config_so100_leader.py | 4 +-- .../so101_leader/config_so101_leader.py | 4 +-- .../teleoperators/so_leader/so_leader_base.py | 4 +-- .../so_leader/so_leader_config_base.py | 7 ++++- src/lerobot/teleoperators/utils.py | 2 +- 26 files changed, 85 insertions(+), 88 deletions(-) delete mode 100644 src/lerobot/robots/bi_so_follower/__init__.py rename src/lerobot/robots/{ => so_follower}/bi_so_follower/bi_so_follower.py (76%) rename src/lerobot/robots/{ => so_follower}/bi_so_follower/config_bi_so_follower.py (90%) delete mode 100644 src/lerobot/teleoperators/bi_so_leader/__init__.py rename src/lerobot/teleoperators/{ => so_leader}/bi_so_leader/bi_so_leader.py (80%) rename src/lerobot/teleoperators/{ => so_leader}/bi_so_leader/config_bi_so_leader.py (90%) diff --git a/docs/source/groot.mdx b/docs/source/groot.mdx index 729a6465643..84d2702bd01 100644 --- a/docs/source/groot.mdx +++ b/docs/source/groot.mdx @@ -103,7 +103,7 @@ Once you have trained your model using your parameters you can run inference in ```bash lerobot-record \ - --robot.type=bi_so100_follower \ + --robot.type=bi_so_follower \ --robot.left_arm_port=/dev/ttyACM1 \ --robot.right_arm_port=/dev/ttyACM0 \ --robot.id=bimanual_follower \ diff --git a/examples/so100_to_so100_EE/record.py b/examples/so100_to_so100_EE/record.py index db24f4b93ab..eead7a9a828 100644 --- a/examples/so100_to_so100_EE/record.py +++ b/examples/so100_to_so100_EE/record.py @@ -34,8 +34,7 @@ InverseKinematicsEEToJoints, ) from lerobot.scripts.lerobot_record import record_loop -from lerobot.teleoperators.so_leader import SO100LeaderConfig -from lerobot.teleoperators.so_leader.so100_leader import SO100Leader +from lerobot.teleoperators.so_leader import SO100Leader, SO100LeaderConfig from lerobot.utils.control_utils import init_keyboard_listener from lerobot.utils.utils import log_say from lerobot.utils.visualization_utils import init_rerun diff --git a/examples/so100_to_so100_EE/teleoperate.py b/examples/so100_to_so100_EE/teleoperate.py index d520a6eaf6e..71d2899de9d 100644 --- a/examples/so100_to_so100_EE/teleoperate.py +++ b/examples/so100_to_so100_EE/teleoperate.py @@ -29,8 +29,7 @@ ForwardKinematicsJointsToEE, InverseKinematicsEEToJoints, ) -from lerobot.teleoperators.so_leader import SO100LeaderConfig -from lerobot.teleoperators.so_leader.so100_leader import SO100Leader +from lerobot.teleoperators.so_leader import SO100Leader, SO100LeaderConfig from lerobot.utils.robot_utils import precise_sleep from lerobot.utils.visualization_utils import init_rerun, log_rerun_data diff --git a/src/lerobot/async_inference/constants.py b/src/lerobot/async_inference/constants.py index f8b6d7bb303..081db050428 100644 --- a/src/lerobot/async_inference/constants.py +++ b/src/lerobot/async_inference/constants.py @@ -26,4 +26,4 @@ SUPPORTED_POLICIES = ["act", "smolvla", "diffusion", "tdmpc", "vqbet", "pi0", "pi05"] # TODO: Add all other robots -SUPPORTED_ROBOTS = ["so100_follower", "so101_follower", "bi_so100_follower", "omx_follower"] +SUPPORTED_ROBOTS = ["so100_follower", "so101_follower", "bi_so_follower", "omx_follower"] diff --git a/src/lerobot/async_inference/robot_client.py b/src/lerobot/async_inference/robot_client.py index eea5585b03d..e4e3e0abc84 100644 --- a/src/lerobot/async_inference/robot_client.py +++ b/src/lerobot/async_inference/robot_client.py @@ -51,7 +51,6 @@ from lerobot.robots import ( # noqa: F401 Robot, RobotConfig, - bi_so_follower, koch_follower, make_robot_from_config, omx_follower, diff --git a/src/lerobot/robots/bi_so_follower/__init__.py b/src/lerobot/robots/bi_so_follower/__init__.py deleted file mode 100644 index d93f7e30a22..00000000000 --- a/src/lerobot/robots/bi_so_follower/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python - -# Copyright 2025 The HuggingFace Inc. team. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from .bi_so_follower import BiSOFollower -from .config_bi_so_follower import BiSOFollowerConfig diff --git a/src/lerobot/robots/so_follower/__init__.py b/src/lerobot/robots/so_follower/__init__.py index 82755250cd7..999e7cacc37 100644 --- a/src/lerobot/robots/so_follower/__init__.py +++ b/src/lerobot/robots/so_follower/__init__.py @@ -14,10 +14,9 @@ # See the License for the specific language governing permissions and # limitations under the License. - +from .bi_so_follower.bi_so_follower import BiSOFollower +from .bi_so_follower.config_bi_so_follower import BiSOFollowerConfig from .so100_follower.config_so100_follower import SO100FollowerConfig from .so100_follower.so100_follower import SO100Follower from .so101_follower.config_so101_follower import SO101FollowerConfig from .so101_follower.so101_follower import SO101Follower -from .so_follower_base import SOFollowerBase -from .so_follower_config_base import SOFollowerConfigBase diff --git a/src/lerobot/robots/bi_so_follower/bi_so_follower.py b/src/lerobot/robots/so_follower/bi_so_follower/bi_so_follower.py similarity index 76% rename from src/lerobot/robots/bi_so_follower/bi_so_follower.py rename to src/lerobot/robots/so_follower/bi_so_follower/bi_so_follower.py index d107d255d03..521ea0793c3 100644 --- a/src/lerobot/robots/bi_so_follower/bi_so_follower.py +++ b/src/lerobot/robots/so_follower/bi_so_follower/bi_so_follower.py @@ -18,9 +18,9 @@ from functools import cached_property from typing import Any -from lerobot.robots.so_follower import SOFollowerBase - -from ..robot import Robot +from ...robot import Robot +from ..so_follower_base import SOFollowerBase +from ..so_follower_config_base import SOFollowerRobotConfigBase from .config_bi_so_follower import BiSOFollowerConfig logger = logging.getLogger(__name__) @@ -38,8 +38,28 @@ def __init__(self, config: BiSOFollowerConfig): super().__init__(config) self.config = config - self.left_arm = SOFollowerBase(self.config.left_arm_config) - self.right_arm = SOFollowerBase(self.config.right_arm_config) + left_arm_config = SOFollowerRobotConfigBase( + id=f"{config.id}_left" if config.id else None, + calibration_dir=config.calibration_dir, + port=config.left_arm_config.port, + disable_torque_on_disconnect=config.left_arm_config.disable_torque_on_disconnect, + max_relative_target=config.left_arm_config.max_relative_target, + use_degrees=config.left_arm_config.use_degrees, + cameras=config.left_arm_config.cameras, + ) + + right_arm_config = SOFollowerRobotConfigBase( + id=f"{config.id}_right" if config.id else None, + calibration_dir=config.calibration_dir, + port=config.right_arm_config.port, + disable_torque_on_disconnect=config.right_arm_config.disable_torque_on_disconnect, + max_relative_target=config.right_arm_config.max_relative_target, + use_degrees=config.right_arm_config.use_degrees, + cameras=config.right_arm_config.cameras, + ) + + self.left_arm = SOFollowerBase(left_arm_config) + self.right_arm = SOFollowerBase(right_arm_config) @property def _motors_ft(self) -> dict[str, type]: diff --git a/src/lerobot/robots/bi_so_follower/config_bi_so_follower.py b/src/lerobot/robots/so_follower/bi_so_follower/config_bi_so_follower.py similarity index 90% rename from src/lerobot/robots/bi_so_follower/config_bi_so_follower.py rename to src/lerobot/robots/so_follower/bi_so_follower/config_bi_so_follower.py index 0f8efb65409..4b2ecc293fb 100644 --- a/src/lerobot/robots/bi_so_follower/config_bi_so_follower.py +++ b/src/lerobot/robots/so_follower/bi_so_follower/config_bi_so_follower.py @@ -16,8 +16,8 @@ from dataclasses import dataclass -from ..config import RobotConfig -from ..so_follower import SOFollowerConfigBase +from ...config import RobotConfig +from ..so_follower_config_base import SOFollowerConfigBase @RobotConfig.register_subclass("bi_so_follower") diff --git a/src/lerobot/robots/so_follower/so100_follower/config_so100_follower.py b/src/lerobot/robots/so_follower/so100_follower/config_so100_follower.py index 3e8dd7e9ff9..f0ffc634ffc 100644 --- a/src/lerobot/robots/so_follower/so100_follower/config_so100_follower.py +++ b/src/lerobot/robots/so_follower/so100_follower/config_so100_follower.py @@ -17,10 +17,10 @@ from dataclasses import dataclass from ...config import RobotConfig -from ..so_follower_config_base import SOFollowerConfigBase +from ..so_follower_config_base import SOFollowerRobotConfigBase @RobotConfig.register_subclass("so100_follower") @dataclass -class SO100FollowerConfig(SOFollowerConfigBase): +class SO100FollowerConfig(SOFollowerRobotConfigBase): pass diff --git a/src/lerobot/robots/so_follower/so101_follower/config_so101_follower.py b/src/lerobot/robots/so_follower/so101_follower/config_so101_follower.py index 950e8c839cb..b2b9d64835d 100644 --- a/src/lerobot/robots/so_follower/so101_follower/config_so101_follower.py +++ b/src/lerobot/robots/so_follower/so101_follower/config_so101_follower.py @@ -17,10 +17,10 @@ from dataclasses import dataclass from ...config import RobotConfig -from ..so_follower_config_base import SOFollowerConfigBase +from ..so_follower_config_base import SOFollowerRobotConfigBase @RobotConfig.register_subclass("so101_follower") @dataclass -class SO101FollowerConfig(SOFollowerConfigBase): +class SO101FollowerConfig(SOFollowerRobotConfigBase): pass diff --git a/src/lerobot/robots/so_follower/so_follower_base.py b/src/lerobot/robots/so_follower/so_follower_base.py index 5e6e3288821..8604798ded9 100644 --- a/src/lerobot/robots/so_follower/so_follower_base.py +++ b/src/lerobot/robots/so_follower/so_follower_base.py @@ -29,7 +29,7 @@ from ..robot import Robot from ..utils import ensure_safe_goal_position -from .so_follower_config_base import SOFollowerConfigBase +from .so_follower_config_base import SOFollowerRobotConfigBase logger = logging.getLogger(__name__) @@ -42,7 +42,7 @@ class SOFollowerBase(Robot): # `config_class` and `name` should be set by subclasses - def __init__(self, config: SOFollowerConfigBase): + def __init__(self, config: SOFollowerRobotConfigBase): super().__init__(config) self.config = config # choose normalization mode depending on config if available diff --git a/src/lerobot/robots/so_follower/so_follower_config_base.py b/src/lerobot/robots/so_follower/so_follower_config_base.py index 6e42df2787f..a57a703a381 100644 --- a/src/lerobot/robots/so_follower/so_follower_config_base.py +++ b/src/lerobot/robots/so_follower/so_follower_config_base.py @@ -22,7 +22,7 @@ @dataclass -class SOFollowerConfigBase(RobotConfig): +class SOFollowerConfigBase: """Base configuration class for SO Follower robots.""" # Port to connect to the arm @@ -40,3 +40,8 @@ class SOFollowerConfigBase(RobotConfig): # Set to `True` for backward compatibility with previous policies/dataset use_degrees: bool = False + + +@dataclass +class SOFollowerRobotConfigBase(RobotConfig, SOFollowerConfigBase): + pass diff --git a/src/lerobot/robots/utils.py b/src/lerobot/robots/utils.py index 27abaaa8690..9fa4b6f8517 100644 --- a/src/lerobot/robots/utils.py +++ b/src/lerobot/robots/utils.py @@ -53,7 +53,7 @@ def make_robot_from_config(config: RobotConfig) -> Robot: return HopeJrArm(config) elif config.type == "bi_so_follower": - from .bi_so_follower import BiSOFollower + from .so_follower import BiSOFollower return BiSOFollower(config) elif config.type == "reachy2": diff --git a/src/lerobot/scripts/lerobot_record.py b/src/lerobot/scripts/lerobot_record.py index 511fa6c1c23..7b101f3aec4 100644 --- a/src/lerobot/scripts/lerobot_record.py +++ b/src/lerobot/scripts/lerobot_record.py @@ -38,7 +38,7 @@ Example recording with bimanual so100: ```shell lerobot-record \ - --robot.type=bi_so100_follower \ + --robot.type=bi_so_follower \ --robot.left_arm_port=/dev/tty.usbmodem5A460851411 \ --robot.right_arm_port=/dev/tty.usbmodem5A460812391 \ --robot.id=bimanual_follower \ @@ -47,7 +47,7 @@ top: {"type": "opencv", "index_or_path": 1, "width": 640, "height": 480, "fps": 30}, right: {"type": "opencv", "index_or_path": 2, "width": 640, "height": 480, "fps": 30} }' \ - --teleop.type=bi_so100_leader \ + --teleop.type=bi_so_leader \ --teleop.left_arm_port=/dev/tty.usbmodem5A460828611 \ --teleop.right_arm_port=/dev/tty.usbmodem5A460826981 \ --teleop.id=bimanual_leader \ @@ -92,7 +92,6 @@ from lerobot.robots import ( # noqa: F401 Robot, RobotConfig, - bi_so_follower, earthrover_mini_plus, hope_jr, koch_follower, @@ -103,7 +102,6 @@ from lerobot.teleoperators import ( # noqa: F401 Teleoperator, TeleoperatorConfig, - bi_so_leader, homunculus, koch_leader, make_teleoperator_from_config, diff --git a/src/lerobot/scripts/lerobot_replay.py b/src/lerobot/scripts/lerobot_replay.py index 35e7b693d91..7808e2b88fb 100644 --- a/src/lerobot/scripts/lerobot_replay.py +++ b/src/lerobot/scripts/lerobot_replay.py @@ -29,7 +29,7 @@ Example replay with bimanual so100: ```shell lerobot-replay \ - --robot.type=bi_so100_follower \ + --robot.type=bi_so_follower \ --robot.left_arm_port=/dev/tty.usbmodem5A460851411 \ --robot.right_arm_port=/dev/tty.usbmodem5A460812391 \ --robot.id=bimanual_follower \ @@ -53,7 +53,6 @@ from lerobot.robots import ( # noqa: F401 Robot, RobotConfig, - bi_so_follower, earthrover_mini_plus, hope_jr, koch_follower, diff --git a/src/lerobot/scripts/lerobot_teleoperate.py b/src/lerobot/scripts/lerobot_teleoperate.py index f743b1747e7..6acf530f001 100644 --- a/src/lerobot/scripts/lerobot_teleoperate.py +++ b/src/lerobot/scripts/lerobot_teleoperate.py @@ -33,7 +33,7 @@ ```shell lerobot-teleoperate \ - --robot.type=bi_so100_follower \ + --robot.type=bi_so_follower \ --robot.left_arm_port=/dev/tty.usbmodem5A460851411 \ --robot.right_arm_port=/dev/tty.usbmodem5A460812391 \ --robot.id=bimanual_follower \ @@ -42,7 +42,7 @@ top: {"type": "opencv", "index_or_path": 1, "width": 1920, "height": 1080, "fps": 30}, right: {"type": "opencv", "index_or_path": 2, "width": 1920, "height": 1080, "fps": 30} }' \ - --teleop.type=bi_so100_leader \ + --teleop.type=bi_so_leader \ --teleop.left_arm_port=/dev/tty.usbmodem5A460828611 \ --teleop.right_arm_port=/dev/tty.usbmodem5A460826981 \ --teleop.id=bimanual_leader \ @@ -70,7 +70,6 @@ from lerobot.robots import ( # noqa: F401 Robot, RobotConfig, - bi_so_follower, earthrover_mini_plus, hope_jr, koch_follower, @@ -81,7 +80,6 @@ from lerobot.teleoperators import ( # noqa: F401 Teleoperator, TeleoperatorConfig, - bi_so_leader, gamepad, homunculus, keyboard, diff --git a/src/lerobot/teleoperators/bi_so_leader/__init__.py b/src/lerobot/teleoperators/bi_so_leader/__init__.py deleted file mode 100644 index 09932aaa520..00000000000 --- a/src/lerobot/teleoperators/bi_so_leader/__init__.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env python - -# Copyright 2025 The HuggingFace Inc. team. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from .bi_so_leader import BiSOLeader -from .config_bi_so_leader import BiSOLeaderConfig diff --git a/src/lerobot/teleoperators/so_leader/__init__.py b/src/lerobot/teleoperators/so_leader/__init__.py index a1017f3b9aa..80ac2236da6 100644 --- a/src/lerobot/teleoperators/so_leader/__init__.py +++ b/src/lerobot/teleoperators/so_leader/__init__.py @@ -14,9 +14,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +from .bi_so_leader.bi_so_leader import BiSOLeader +from .bi_so_leader.config_bi_so_leader import BiSOLeaderConfig from .so100_leader.config_so100_leader import SO100LeaderConfig from .so100_leader.so100_leader import SO100Leader from .so101_leader.config_so101_leader import SO101LeaderConfig from .so101_leader.so101_leader import SO101Leader -from .so_leader_base import SOLeaderBase -from .so_leader_config_base import SOLeaderConfigBase diff --git a/src/lerobot/teleoperators/bi_so_leader/bi_so_leader.py b/src/lerobot/teleoperators/so_leader/bi_so_leader/bi_so_leader.py similarity index 80% rename from src/lerobot/teleoperators/bi_so_leader/bi_so_leader.py rename to src/lerobot/teleoperators/so_leader/bi_so_leader/bi_so_leader.py index bf1d5ed1111..992dfb67e11 100644 --- a/src/lerobot/teleoperators/bi_so_leader/bi_so_leader.py +++ b/src/lerobot/teleoperators/so_leader/bi_so_leader/bi_so_leader.py @@ -17,9 +17,9 @@ import logging from functools import cached_property -from lerobot.teleoperators.so_leader import SOLeaderBase - -from ..teleoperator import Teleoperator +from ...teleoperator import Teleoperator +from ..so_leader_base import SOLeaderBase +from ..so_leader_config_base import SOLeaderTeleopConfigBase from .config_bi_so_leader import BiSOLeaderConfig logger = logging.getLogger(__name__) @@ -37,8 +37,20 @@ def __init__(self, config: BiSOLeaderConfig): super().__init__(config) self.config = config - self.left_arm = SOLeaderBase(self.config.left_arm_config) - self.right_arm = SOLeaderBase(self.config.right_arm_config) + left_arm_config = SOLeaderTeleopConfigBase( + id=f"{config.id}_left" if config.id else None, + calibration_dir=config.calibration_dir, + port=config.left_arm_config.port, + ) + + right_arm_config = SOLeaderTeleopConfigBase( + id=f"{config.id}_right" if config.id else None, + calibration_dir=config.calibration_dir, + port=config.right_arm_config.port, + ) + + self.left_arm = SOLeaderBase(left_arm_config) + self.right_arm = SOLeaderBase(right_arm_config) @cached_property def action_features(self) -> dict[str, type]: diff --git a/src/lerobot/teleoperators/bi_so_leader/config_bi_so_leader.py b/src/lerobot/teleoperators/so_leader/bi_so_leader/config_bi_so_leader.py similarity index 90% rename from src/lerobot/teleoperators/bi_so_leader/config_bi_so_leader.py rename to src/lerobot/teleoperators/so_leader/bi_so_leader/config_bi_so_leader.py index d1d24fb34b3..c2e84a4fb8b 100644 --- a/src/lerobot/teleoperators/bi_so_leader/config_bi_so_leader.py +++ b/src/lerobot/teleoperators/so_leader/bi_so_leader/config_bi_so_leader.py @@ -16,8 +16,8 @@ from dataclasses import dataclass -from ..config import TeleoperatorConfig -from ..so_leader import SOLeaderConfigBase +from ...config import TeleoperatorConfig +from ..so_leader_config_base import SOLeaderConfigBase @TeleoperatorConfig.register_subclass("bi_so_leader") diff --git a/src/lerobot/teleoperators/so_leader/so100_leader/config_so100_leader.py b/src/lerobot/teleoperators/so_leader/so100_leader/config_so100_leader.py index 092eb0cfc03..4503bc6a79e 100644 --- a/src/lerobot/teleoperators/so_leader/so100_leader/config_so100_leader.py +++ b/src/lerobot/teleoperators/so_leader/so100_leader/config_so100_leader.py @@ -17,10 +17,10 @@ from dataclasses import dataclass from ...config import TeleoperatorConfig -from ..so_leader_config_base import SOLeaderConfigBase +from ..so_leader_config_base import SOLeaderTeleopConfigBase @TeleoperatorConfig.register_subclass("so100_leader") @dataclass -class SO100LeaderConfig(SOLeaderConfigBase): +class SO100LeaderConfig(SOLeaderTeleopConfigBase): pass diff --git a/src/lerobot/teleoperators/so_leader/so101_leader/config_so101_leader.py b/src/lerobot/teleoperators/so_leader/so101_leader/config_so101_leader.py index 1a6bb0df475..1e110d73e85 100644 --- a/src/lerobot/teleoperators/so_leader/so101_leader/config_so101_leader.py +++ b/src/lerobot/teleoperators/so_leader/so101_leader/config_so101_leader.py @@ -17,10 +17,10 @@ from dataclasses import dataclass from ...config import TeleoperatorConfig -from ..so_leader_config_base import SOLeaderConfigBase +from ..so_leader_config_base import SOLeaderTeleopConfigBase @TeleoperatorConfig.register_subclass("so101_leader") @dataclass -class SO101LeaderConfig(SOLeaderConfigBase): +class SO101LeaderConfig(SOLeaderTeleopConfigBase): pass diff --git a/src/lerobot/teleoperators/so_leader/so_leader_base.py b/src/lerobot/teleoperators/so_leader/so_leader_base.py index 6cdaca2e760..770ee79769c 100644 --- a/src/lerobot/teleoperators/so_leader/so_leader_base.py +++ b/src/lerobot/teleoperators/so_leader/so_leader_base.py @@ -25,7 +25,7 @@ from lerobot.utils.errors import DeviceAlreadyConnectedError, DeviceNotConnectedError from ..teleoperator import Teleoperator -from .so_leader_config_base import SOLeaderConfigBase +from .so_leader_config_base import SOLeaderTeleopConfigBase logger = logging.getLogger(__name__) @@ -33,7 +33,7 @@ class SOLeaderBase(Teleoperator): """Generic SO leader base for SO-100/101/10X teleoperators.""" - def __init__(self, config: SOLeaderConfigBase): + def __init__(self, config: SOLeaderTeleopConfigBase): super().__init__(config) self.config = config norm_mode_body = MotorNormMode.DEGREES if config.use_degrees else MotorNormMode.RANGE_M100_100 diff --git a/src/lerobot/teleoperators/so_leader/so_leader_config_base.py b/src/lerobot/teleoperators/so_leader/so_leader_config_base.py index b2f2dfcfa77..db58c18d5f5 100644 --- a/src/lerobot/teleoperators/so_leader/so_leader_config_base.py +++ b/src/lerobot/teleoperators/so_leader/so_leader_config_base.py @@ -20,7 +20,7 @@ @dataclass -class SOLeaderConfigBase(TeleoperatorConfig): +class SOLeaderConfigBase: """Base configuration class for SO Leader teleoperators.""" # Port to connect to the arm @@ -28,3 +28,8 @@ class SOLeaderConfigBase(TeleoperatorConfig): # Whether to use degrees for angles use_degrees: bool = False + + +@dataclass +class SOLeaderTeleopConfigBase(TeleoperatorConfig, SOLeaderConfigBase): + pass diff --git a/src/lerobot/teleoperators/utils.py b/src/lerobot/teleoperators/utils.py index eec2f119c63..5baae36e2f2 100644 --- a/src/lerobot/teleoperators/utils.py +++ b/src/lerobot/teleoperators/utils.py @@ -74,7 +74,7 @@ def make_teleoperator_from_config(config: TeleoperatorConfig) -> Teleoperator: return HomunculusArm(config) elif config.type == "bi_so_leader": - from .bi_so_leader import BiSOLeader + from .so_leader import BiSOLeader return BiSOLeader(config) elif config.type == "reachy2_teleoperator": From f6b5f4dfac4d528f7792251a3eb8146bcc6052ed Mon Sep 17 00:00:00 2001 From: Steven Palma Date: Fri, 9 Jan 2026 18:32:55 +0100 Subject: [PATCH 3/7] fix(robots): teleop & record working --- .../bi_so_follower/bi_so_follower.py | 3 +++ .../robots/so_follower/so_follower_base.py | 3 ++- src/lerobot/scripts/lerobot_record.py | 20 ++++++++++--------- src/lerobot/scripts/lerobot_teleoperate.py | 16 +++++++-------- .../teleoperators/so_leader/so_leader_base.py | 3 +++ 5 files changed, 27 insertions(+), 18 deletions(-) diff --git a/src/lerobot/robots/so_follower/bi_so_follower/bi_so_follower.py b/src/lerobot/robots/so_follower/bi_so_follower/bi_so_follower.py index 521ea0793c3..6d2cbd76357 100644 --- a/src/lerobot/robots/so_follower/bi_so_follower/bi_so_follower.py +++ b/src/lerobot/robots/so_follower/bi_so_follower/bi_so_follower.py @@ -61,6 +61,9 @@ def __init__(self, config: BiSOFollowerConfig): self.left_arm = SOFollowerBase(left_arm_config) self.right_arm = SOFollowerBase(right_arm_config) + # Only for compatibility with other parts of the codebase that expect a `robot.cameras` attribute + self.cameras = {**self.left_arm.cameras, **self.right_arm.cameras} + @property def _motors_ft(self) -> dict[str, type]: left_arm_motors_ft = self.left_arm._motors_ft diff --git a/src/lerobot/robots/so_follower/so_follower_base.py b/src/lerobot/robots/so_follower/so_follower_base.py index 8604798ded9..3caaf8c0138 100644 --- a/src/lerobot/robots/so_follower/so_follower_base.py +++ b/src/lerobot/robots/so_follower/so_follower_base.py @@ -40,7 +40,8 @@ class SOFollowerBase(Robot): Designed to be subclassed with a per-hardware-model `config_class` and `name`. """ - # `config_class` and `name` should be set by subclasses + config_class = SOFollowerRobotConfigBase + name = "so_follower" def __init__(self, config: SOFollowerRobotConfigBase): super().__init__(config) diff --git a/src/lerobot/scripts/lerobot_record.py b/src/lerobot/scripts/lerobot_record.py index 7b101f3aec4..45cec718030 100644 --- a/src/lerobot/scripts/lerobot_record.py +++ b/src/lerobot/scripts/lerobot_record.py @@ -39,20 +39,22 @@ ```shell lerobot-record \ --robot.type=bi_so_follower \ - --robot.left_arm_port=/dev/tty.usbmodem5A460851411 \ - --robot.right_arm_port=/dev/tty.usbmodem5A460812391 \ + --robot.left_arm_config.port=/dev/tty.usbmodem5A460822851 \ + --robot.right_arm_config.port=/dev/tty.usbmodem5A460814411 \ --robot.id=bimanual_follower \ - --robot.cameras='{ - left: {"type": "opencv", "index_or_path": 0, "width": 640, "height": 480, "fps": 30}, - top: {"type": "opencv", "index_or_path": 1, "width": 640, "height": 480, "fps": 30}, - right: {"type": "opencv", "index_or_path": 2, "width": 640, "height": 480, "fps": 30} + --robot.left_arm_config.cameras='{ + wrist: {"type": "opencv", "index_or_path": 1, "width": 640, "height": 480, "fps": 30}, + top: {"type": "opencv", "index_or_path": 3, "width": 640, "height": 480, "fps": 30}, + }' --robot.right_arm_config.cameras='{ + wrist: {"type": "opencv", "index_or_path": 2, "width": 640, "height": 480, "fps": 30}, + front: {"type": "opencv", "index_or_path": 4, "width": 640, "height": 480, "fps": 30}, }' \ --teleop.type=bi_so_leader \ - --teleop.left_arm_port=/dev/tty.usbmodem5A460828611 \ - --teleop.right_arm_port=/dev/tty.usbmodem5A460826981 \ + --teleop.left_arm_config.port=/dev/tty.usbmodem5A460852721 \ + --teleop.right_arm_config.port=/dev/tty.usbmodem5A460819811 \ --teleop.id=bimanual_leader \ --display_data=true \ - --dataset.repo_id=${HF_USER}/bimanual-so100-handover-cube \ + --dataset.repo_id=${HF_USER}/bimanual-so-handover-cube \ --dataset.num_episodes=25 \ --dataset.single_task="Grab and handover the red cube to the other arm" ``` diff --git a/src/lerobot/scripts/lerobot_teleoperate.py b/src/lerobot/scripts/lerobot_teleoperate.py index 6acf530f001..835f1c69285 100644 --- a/src/lerobot/scripts/lerobot_teleoperate.py +++ b/src/lerobot/scripts/lerobot_teleoperate.py @@ -34,17 +34,17 @@ ```shell lerobot-teleoperate \ --robot.type=bi_so_follower \ - --robot.left_arm_port=/dev/tty.usbmodem5A460851411 \ - --robot.right_arm_port=/dev/tty.usbmodem5A460812391 \ + --robot.left_arm_config.port=/dev/tty.usbmodem5A460822851 \ + --robot.right_arm_config.port=/dev/tty.usbmodem5A460814411 \ --robot.id=bimanual_follower \ - --robot.cameras='{ - left: {"type": "opencv", "index_or_path": 0, "width": 1920, "height": 1080, "fps": 30}, - top: {"type": "opencv", "index_or_path": 1, "width": 1920, "height": 1080, "fps": 30}, - right: {"type": "opencv", "index_or_path": 2, "width": 1920, "height": 1080, "fps": 30} + --robot.left_arm_config.cameras='{ + wrist: {"type": "opencv", "index_or_path": 1, "width": 640, "height": 480, "fps": 30}, + }' --robot.right_arm_config.cameras='{ + wrist: {"type": "opencv", "index_or_path": 2, "width": 640, "height": 480, "fps": 30}, }' \ --teleop.type=bi_so_leader \ - --teleop.left_arm_port=/dev/tty.usbmodem5A460828611 \ - --teleop.right_arm_port=/dev/tty.usbmodem5A460826981 \ + --teleop.left_arm_config.port=/dev/tty.usbmodem5A460852721 \ + --teleop.right_arm_config.port=/dev/tty.usbmodem5A460819811 \ --teleop.id=bimanual_leader \ --display_data=true ``` diff --git a/src/lerobot/teleoperators/so_leader/so_leader_base.py b/src/lerobot/teleoperators/so_leader/so_leader_base.py index 770ee79769c..01936d8ac05 100644 --- a/src/lerobot/teleoperators/so_leader/so_leader_base.py +++ b/src/lerobot/teleoperators/so_leader/so_leader_base.py @@ -33,6 +33,9 @@ class SOLeaderBase(Teleoperator): """Generic SO leader base for SO-100/101/10X teleoperators.""" + config_class = SOLeaderTeleopConfigBase + name = "so_leader" + def __init__(self, config: SOLeaderTeleopConfigBase): super().__init__(config) self.config = config From 2d534249294abe379465a9f63258ae78d1233cf0 Mon Sep 17 00:00:00 2001 From: Steven Palma Date: Mon, 12 Jan 2026 11:47:17 +0100 Subject: [PATCH 4/7] feat(robots): only one SO --- .../__init__.py} | 12 ++------- .../bi_so_follower/bi_so_follower.py | 14 +++++----- .../bi_so_follower/config_bi_so_follower.py | 9 ++++--- src/lerobot/robots/so_follower/__init__.py | 13 ++++----- ...r_config_base.py => config_so_follower.py} | 11 ++++++-- src/lerobot/robots/so_follower/so100.md | 1 + .../so_follower/so100_follower/so100.md | 1 - .../so100_follower/so100_follower.py | 27 ------------------- src/lerobot/robots/so_follower/so101.md | 1 + .../so_follower/so101_follower/so101.md | 1 - .../so101_follower/so101_follower.py | 27 ------------------- .../{so_follower_base.py => so_follower.py} | 14 ++++++---- .../bi_so_leader/__init__.py} | 11 +------- .../bi_so_leader/bi_so_leader.py | 15 ++++++----- .../bi_so_leader/config_bi_so_leader.py | 9 ++++--- .../teleoperators/so_leader/__init__.py | 13 ++++----- ...der_config_base.py => config_so_leader.py} | 11 ++++++-- src/lerobot/teleoperators/so_leader/so100.md | 1 + .../so100_leader/config_so100_leader.py | 26 ------------------ .../so_leader/so100_leader/so100.md | 1 - .../so_leader/so100_leader/so100_leader.py | 27 ------------------- src/lerobot/teleoperators/so_leader/so101.md | 1 + .../so101_leader/config_so101_leader.py | 26 ------------------ .../so_leader/so101_leader/so101.md | 1 - .../so_leader/so101_leader/so101_leader.py | 27 ------------------- .../{so_leader_base.py => so_leader.py} | 13 ++++++--- tests/robots/test_so100_follower.py | 2 +- 27 files changed, 83 insertions(+), 232 deletions(-) rename src/lerobot/robots/{so_follower/so100_follower/config_so100_follower.py => bi_so_follower/__init__.py} (71%) rename src/lerobot/robots/{so_follower => }/bi_so_follower/bi_so_follower.py (93%) rename src/lerobot/robots/{so_follower => }/bi_so_follower/config_bi_so_follower.py (82%) rename src/lerobot/robots/so_follower/{so_follower_config_base.py => config_so_follower.py} (82%) create mode 120000 src/lerobot/robots/so_follower/so100.md delete mode 120000 src/lerobot/robots/so_follower/so100_follower/so100.md delete mode 100644 src/lerobot/robots/so_follower/so100_follower/so100_follower.py create mode 120000 src/lerobot/robots/so_follower/so101.md delete mode 120000 src/lerobot/robots/so_follower/so101_follower/so101.md delete mode 100644 src/lerobot/robots/so_follower/so101_follower/so101_follower.py rename src/lerobot/robots/so_follower/{so_follower_base.py => so_follower.py} (97%) rename src/lerobot/{robots/so_follower/so101_follower/config_so101_follower.py => teleoperators/bi_so_leader/__init__.py} (71%) rename src/lerobot/teleoperators/{so_leader => }/bi_so_leader/bi_so_leader.py (90%) rename src/lerobot/teleoperators/{so_leader => }/bi_so_leader/config_bi_so_leader.py (82%) rename src/lerobot/teleoperators/so_leader/{so_leader_config_base.py => config_so_leader.py} (73%) create mode 120000 src/lerobot/teleoperators/so_leader/so100.md delete mode 100644 src/lerobot/teleoperators/so_leader/so100_leader/config_so100_leader.py delete mode 120000 src/lerobot/teleoperators/so_leader/so100_leader/so100.md delete mode 100644 src/lerobot/teleoperators/so_leader/so100_leader/so100_leader.py create mode 120000 src/lerobot/teleoperators/so_leader/so101.md delete mode 100644 src/lerobot/teleoperators/so_leader/so101_leader/config_so101_leader.py delete mode 120000 src/lerobot/teleoperators/so_leader/so101_leader/so101.md delete mode 100644 src/lerobot/teleoperators/so_leader/so101_leader/so101_leader.py rename src/lerobot/teleoperators/so_leader/{so_leader_base.py => so_leader.py} (95%) diff --git a/src/lerobot/robots/so_follower/so100_follower/config_so100_follower.py b/src/lerobot/robots/bi_so_follower/__init__.py similarity index 71% rename from src/lerobot/robots/so_follower/so100_follower/config_so100_follower.py rename to src/lerobot/robots/bi_so_follower/__init__.py index f0ffc634ffc..f631a14db51 100644 --- a/src/lerobot/robots/so_follower/so100_follower/config_so100_follower.py +++ b/src/lerobot/robots/bi_so_follower/__init__.py @@ -14,13 +14,5 @@ # See the License for the specific language governing permissions and # limitations under the License. -from dataclasses import dataclass - -from ...config import RobotConfig -from ..so_follower_config_base import SOFollowerRobotConfigBase - - -@RobotConfig.register_subclass("so100_follower") -@dataclass -class SO100FollowerConfig(SOFollowerRobotConfigBase): - pass +from .bi_so_follower import BiSOFollower +from .config_bi_so_follower import BiSOFollowerConfig diff --git a/src/lerobot/robots/so_follower/bi_so_follower/bi_so_follower.py b/src/lerobot/robots/bi_so_follower/bi_so_follower.py similarity index 93% rename from src/lerobot/robots/so_follower/bi_so_follower/bi_so_follower.py rename to src/lerobot/robots/bi_so_follower/bi_so_follower.py index 6d2cbd76357..fa81e7d0919 100644 --- a/src/lerobot/robots/so_follower/bi_so_follower/bi_so_follower.py +++ b/src/lerobot/robots/bi_so_follower/bi_so_follower.py @@ -18,9 +18,9 @@ from functools import cached_property from typing import Any -from ...robot import Robot -from ..so_follower_base import SOFollowerBase -from ..so_follower_config_base import SOFollowerRobotConfigBase +from lerobot.robots.so_follower import SOFollower, SOFollowerRobotConfig + +from ..robot import Robot from .config_bi_so_follower import BiSOFollowerConfig logger = logging.getLogger(__name__) @@ -38,7 +38,7 @@ def __init__(self, config: BiSOFollowerConfig): super().__init__(config) self.config = config - left_arm_config = SOFollowerRobotConfigBase( + left_arm_config = SOFollowerRobotConfig( id=f"{config.id}_left" if config.id else None, calibration_dir=config.calibration_dir, port=config.left_arm_config.port, @@ -48,7 +48,7 @@ def __init__(self, config: BiSOFollowerConfig): cameras=config.left_arm_config.cameras, ) - right_arm_config = SOFollowerRobotConfigBase( + right_arm_config = SOFollowerRobotConfig( id=f"{config.id}_right" if config.id else None, calibration_dir=config.calibration_dir, port=config.right_arm_config.port, @@ -58,8 +58,8 @@ def __init__(self, config: BiSOFollowerConfig): cameras=config.right_arm_config.cameras, ) - self.left_arm = SOFollowerBase(left_arm_config) - self.right_arm = SOFollowerBase(right_arm_config) + self.left_arm = SOFollower(left_arm_config) + self.right_arm = SOFollower(right_arm_config) # Only for compatibility with other parts of the codebase that expect a `robot.cameras` attribute self.cameras = {**self.left_arm.cameras, **self.right_arm.cameras} diff --git a/src/lerobot/robots/so_follower/bi_so_follower/config_bi_so_follower.py b/src/lerobot/robots/bi_so_follower/config_bi_so_follower.py similarity index 82% rename from src/lerobot/robots/so_follower/bi_so_follower/config_bi_so_follower.py rename to src/lerobot/robots/bi_so_follower/config_bi_so_follower.py index 4b2ecc293fb..dca74fa2de4 100644 --- a/src/lerobot/robots/so_follower/bi_so_follower/config_bi_so_follower.py +++ b/src/lerobot/robots/bi_so_follower/config_bi_so_follower.py @@ -16,8 +16,9 @@ from dataclasses import dataclass -from ...config import RobotConfig -from ..so_follower_config_base import SOFollowerConfigBase +from lerobot.robots.so_follower import SOFollowerConfig + +from ..config import RobotConfig @RobotConfig.register_subclass("bi_so_follower") @@ -25,5 +26,5 @@ class BiSOFollowerConfig(RobotConfig): """Configuration class for Bi SO Follower robots.""" - left_arm_config: SOFollowerConfigBase - right_arm_config: SOFollowerConfigBase + left_arm_config: SOFollowerConfig + right_arm_config: SOFollowerConfig diff --git a/src/lerobot/robots/so_follower/__init__.py b/src/lerobot/robots/so_follower/__init__.py index 999e7cacc37..eea2fcbdf95 100644 --- a/src/lerobot/robots/so_follower/__init__.py +++ b/src/lerobot/robots/so_follower/__init__.py @@ -14,9 +14,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .bi_so_follower.bi_so_follower import BiSOFollower -from .bi_so_follower.config_bi_so_follower import BiSOFollowerConfig -from .so100_follower.config_so100_follower import SO100FollowerConfig -from .so100_follower.so100_follower import SO100Follower -from .so101_follower.config_so101_follower import SO101FollowerConfig -from .so101_follower.so101_follower import SO101Follower +from .config_so_follower import ( + SO100FollowerConfig, + SO101FollowerConfig, + SOFollowerConfig, + SOFollowerRobotConfig, +) +from .so_follower import SO100Follower, SO101Follower, SOFollower diff --git a/src/lerobot/robots/so_follower/so_follower_config_base.py b/src/lerobot/robots/so_follower/config_so_follower.py similarity index 82% rename from src/lerobot/robots/so_follower/so_follower_config_base.py rename to src/lerobot/robots/so_follower/config_so_follower.py index a57a703a381..e9ce27123cb 100644 --- a/src/lerobot/robots/so_follower/so_follower_config_base.py +++ b/src/lerobot/robots/so_follower/config_so_follower.py @@ -15,6 +15,7 @@ # limitations under the License. from dataclasses import dataclass, field +from typing import TypeAlias from lerobot.cameras import CameraConfig @@ -22,7 +23,7 @@ @dataclass -class SOFollowerConfigBase: +class SOFollowerConfig: """Base configuration class for SO Follower robots.""" # Port to connect to the arm @@ -42,6 +43,12 @@ class SOFollowerConfigBase: use_degrees: bool = False +@RobotConfig.register_subclass("so101_follower") +@RobotConfig.register_subclass("so100_follower") @dataclass -class SOFollowerRobotConfigBase(RobotConfig, SOFollowerConfigBase): +class SOFollowerRobotConfig(RobotConfig, SOFollowerConfig): pass + + +SO100FollowerConfig: TypeAlias = SOFollowerRobotConfig +SO101FollowerConfig: TypeAlias = SOFollowerRobotConfig diff --git a/src/lerobot/robots/so_follower/so100.md b/src/lerobot/robots/so_follower/so100.md new file mode 120000 index 00000000000..ad1154e75a7 --- /dev/null +++ b/src/lerobot/robots/so_follower/so100.md @@ -0,0 +1 @@ +../../../../docs/source/so100.mdx \ No newline at end of file diff --git a/src/lerobot/robots/so_follower/so100_follower/so100.md b/src/lerobot/robots/so_follower/so100_follower/so100.md deleted file mode 120000 index f06f88ff63e..00000000000 --- a/src/lerobot/robots/so_follower/so100_follower/so100.md +++ /dev/null @@ -1 +0,0 @@ -../../../../../docs/source/so100.mdx \ No newline at end of file diff --git a/src/lerobot/robots/so_follower/so100_follower/so100_follower.py b/src/lerobot/robots/so_follower/so100_follower/so100_follower.py deleted file mode 100644 index ef61f5ce382..00000000000 --- a/src/lerobot/robots/so_follower/so100_follower/so100_follower.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python - -# Copyright 2026 The HuggingFace Inc. team. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from ..so_follower_base import SOFollowerBase -from .config_so100_follower import SO100FollowerConfig - - -class SO100Follower(SOFollowerBase): - """ - SO-101 follower robot class. [SO-101 Follower Arm](https://github.com/TheRobotStudio/SO-ARM100) designed by TheRobotStudio - """ - - config_class = SO100FollowerConfig - name = "so100_follower" diff --git a/src/lerobot/robots/so_follower/so101.md b/src/lerobot/robots/so_follower/so101.md new file mode 120000 index 00000000000..27b89266029 --- /dev/null +++ b/src/lerobot/robots/so_follower/so101.md @@ -0,0 +1 @@ +../../../../docs/source/so101.mdx \ No newline at end of file diff --git a/src/lerobot/robots/so_follower/so101_follower/so101.md b/src/lerobot/robots/so_follower/so101_follower/so101.md deleted file mode 120000 index 38f4deca725..00000000000 --- a/src/lerobot/robots/so_follower/so101_follower/so101.md +++ /dev/null @@ -1 +0,0 @@ -../../../../../docs/source/so101.mdx \ No newline at end of file diff --git a/src/lerobot/robots/so_follower/so101_follower/so101_follower.py b/src/lerobot/robots/so_follower/so101_follower/so101_follower.py deleted file mode 100644 index b4cfb2711df..00000000000 --- a/src/lerobot/robots/so_follower/so101_follower/so101_follower.py +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env python - -# Copyright 2026 The HuggingFace Inc. team. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from ..so_follower_base import SOFollowerBase -from .config_so101_follower import SO101FollowerConfig - - -class SO101Follower(SOFollowerBase): - """ - SO-101 follower robot class. [SO-101 Follower Arm](https://github.com/TheRobotStudio/SO-ARM100) designed by TheRobotStudio - """ - - config_class = SO101FollowerConfig - name = "so101_follower" diff --git a/src/lerobot/robots/so_follower/so_follower_base.py b/src/lerobot/robots/so_follower/so_follower.py similarity index 97% rename from src/lerobot/robots/so_follower/so_follower_base.py rename to src/lerobot/robots/so_follower/so_follower.py index 3caaf8c0138..5e99b33a1b8 100644 --- a/src/lerobot/robots/so_follower/so_follower_base.py +++ b/src/lerobot/robots/so_follower/so_follower.py @@ -17,7 +17,7 @@ import logging import time from functools import cached_property -from typing import Any +from typing import Any, TypeAlias from lerobot.cameras.utils import make_cameras_from_configs from lerobot.motors import Motor, MotorCalibration, MotorNormMode @@ -29,21 +29,21 @@ from ..robot import Robot from ..utils import ensure_safe_goal_position -from .so_follower_config_base import SOFollowerRobotConfigBase +from .config_so_follower import SOFollowerRobotConfig logger = logging.getLogger(__name__) -class SOFollowerBase(Robot): +class SOFollower(Robot): """ Generic SO follower base implementing common functionality for SO-100/101/10X. Designed to be subclassed with a per-hardware-model `config_class` and `name`. """ - config_class = SOFollowerRobotConfigBase + config_class = SOFollowerRobotConfig name = "so_follower" - def __init__(self, config: SOFollowerRobotConfigBase): + def __init__(self, config: SOFollowerRobotConfig): super().__init__(config) self.config = config # choose normalization mode depending on config if available @@ -233,3 +233,7 @@ def disconnect(self): cam.disconnect() logger.info(f"{self} disconnected.") + + +SO100Follower: TypeAlias = SOFollower +SO101Follower: TypeAlias = SOFollower diff --git a/src/lerobot/robots/so_follower/so101_follower/config_so101_follower.py b/src/lerobot/teleoperators/bi_so_leader/__init__.py similarity index 71% rename from src/lerobot/robots/so_follower/so101_follower/config_so101_follower.py rename to src/lerobot/teleoperators/bi_so_leader/__init__.py index b2b9d64835d..b902270f945 100644 --- a/src/lerobot/robots/so_follower/so101_follower/config_so101_follower.py +++ b/src/lerobot/teleoperators/bi_so_leader/__init__.py @@ -14,13 +14,4 @@ # See the License for the specific language governing permissions and # limitations under the License. -from dataclasses import dataclass - -from ...config import RobotConfig -from ..so_follower_config_base import SOFollowerRobotConfigBase - - -@RobotConfig.register_subclass("so101_follower") -@dataclass -class SO101FollowerConfig(SOFollowerRobotConfigBase): - pass +from .bi_so_leader import BiSOLeader, BiSOLeaderConfig diff --git a/src/lerobot/teleoperators/so_leader/bi_so_leader/bi_so_leader.py b/src/lerobot/teleoperators/bi_so_leader/bi_so_leader.py similarity index 90% rename from src/lerobot/teleoperators/so_leader/bi_so_leader/bi_so_leader.py rename to src/lerobot/teleoperators/bi_so_leader/bi_so_leader.py index 992dfb67e11..45c46c100c8 100644 --- a/src/lerobot/teleoperators/so_leader/bi_so_leader/bi_so_leader.py +++ b/src/lerobot/teleoperators/bi_so_leader/bi_so_leader.py @@ -17,9 +17,10 @@ import logging from functools import cached_property -from ...teleoperator import Teleoperator -from ..so_leader_base import SOLeaderBase -from ..so_leader_config_base import SOLeaderTeleopConfigBase +from lerobot.teleoperators.so_leader import SOLeaderTeleopConfig + +from ..so_leader import SOLeader +from ..teleoperator import Teleoperator from .config_bi_so_leader import BiSOLeaderConfig logger = logging.getLogger(__name__) @@ -37,20 +38,20 @@ def __init__(self, config: BiSOLeaderConfig): super().__init__(config) self.config = config - left_arm_config = SOLeaderTeleopConfigBase( + left_arm_config = SOLeaderTeleopConfig( id=f"{config.id}_left" if config.id else None, calibration_dir=config.calibration_dir, port=config.left_arm_config.port, ) - right_arm_config = SOLeaderTeleopConfigBase( + right_arm_config = SOLeaderTeleopConfig( id=f"{config.id}_right" if config.id else None, calibration_dir=config.calibration_dir, port=config.right_arm_config.port, ) - self.left_arm = SOLeaderBase(left_arm_config) - self.right_arm = SOLeaderBase(right_arm_config) + self.left_arm = SOLeader(left_arm_config) + self.right_arm = SOLeader(right_arm_config) @cached_property def action_features(self) -> dict[str, type]: diff --git a/src/lerobot/teleoperators/so_leader/bi_so_leader/config_bi_so_leader.py b/src/lerobot/teleoperators/bi_so_leader/config_bi_so_leader.py similarity index 82% rename from src/lerobot/teleoperators/so_leader/bi_so_leader/config_bi_so_leader.py rename to src/lerobot/teleoperators/bi_so_leader/config_bi_so_leader.py index c2e84a4fb8b..c2f23c617b6 100644 --- a/src/lerobot/teleoperators/so_leader/bi_so_leader/config_bi_so_leader.py +++ b/src/lerobot/teleoperators/bi_so_leader/config_bi_so_leader.py @@ -16,8 +16,9 @@ from dataclasses import dataclass -from ...config import TeleoperatorConfig -from ..so_leader_config_base import SOLeaderConfigBase +from lerobot.teleoperators.so_leader import SOLeaderConfig + +from ..config import TeleoperatorConfig @TeleoperatorConfig.register_subclass("bi_so_leader") @@ -25,5 +26,5 @@ class BiSOLeaderConfig(TeleoperatorConfig): """Configuration class for Bi SO Leader teleoperators.""" - left_arm_config: SOLeaderConfigBase - right_arm_config: SOLeaderConfigBase + left_arm_config: SOLeaderConfig + right_arm_config: SOLeaderConfig diff --git a/src/lerobot/teleoperators/so_leader/__init__.py b/src/lerobot/teleoperators/so_leader/__init__.py index 80ac2236da6..e5aaa31b62b 100644 --- a/src/lerobot/teleoperators/so_leader/__init__.py +++ b/src/lerobot/teleoperators/so_leader/__init__.py @@ -14,9 +14,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .bi_so_leader.bi_so_leader import BiSOLeader -from .bi_so_leader.config_bi_so_leader import BiSOLeaderConfig -from .so100_leader.config_so100_leader import SO100LeaderConfig -from .so100_leader.so100_leader import SO100Leader -from .so101_leader.config_so101_leader import SO101LeaderConfig -from .so101_leader.so101_leader import SO101Leader +from .config_so_leader import ( + SO100LeaderConfig, + SO101LeaderConfig, + SOLeaderConfig, + SOLeaderTeleopConfig, +) +from .so_leader import SO100Leader, SO101Leader, SOLeader diff --git a/src/lerobot/teleoperators/so_leader/so_leader_config_base.py b/src/lerobot/teleoperators/so_leader/config_so_leader.py similarity index 73% rename from src/lerobot/teleoperators/so_leader/so_leader_config_base.py rename to src/lerobot/teleoperators/so_leader/config_so_leader.py index db58c18d5f5..dd55196d7b6 100644 --- a/src/lerobot/teleoperators/so_leader/so_leader_config_base.py +++ b/src/lerobot/teleoperators/so_leader/config_so_leader.py @@ -15,12 +15,13 @@ # limitations under the License. from dataclasses import dataclass +from typing import TypeAlias from ..config import TeleoperatorConfig @dataclass -class SOLeaderConfigBase: +class SOLeaderConfig: """Base configuration class for SO Leader teleoperators.""" # Port to connect to the arm @@ -30,6 +31,12 @@ class SOLeaderConfigBase: use_degrees: bool = False +@TeleoperatorConfig.register_subclass("so101_leader") +@TeleoperatorConfig.register_subclass("so100_leader") @dataclass -class SOLeaderTeleopConfigBase(TeleoperatorConfig, SOLeaderConfigBase): +class SOLeaderTeleopConfig(TeleoperatorConfig, SOLeaderConfig): pass + + +SO100LeaderConfig: TypeAlias = SOLeaderTeleopConfig +SO101LeaderConfig: TypeAlias = SOLeaderTeleopConfig diff --git a/src/lerobot/teleoperators/so_leader/so100.md b/src/lerobot/teleoperators/so_leader/so100.md new file mode 120000 index 00000000000..ad1154e75a7 --- /dev/null +++ b/src/lerobot/teleoperators/so_leader/so100.md @@ -0,0 +1 @@ +../../../../docs/source/so100.mdx \ No newline at end of file diff --git a/src/lerobot/teleoperators/so_leader/so100_leader/config_so100_leader.py b/src/lerobot/teleoperators/so_leader/so100_leader/config_so100_leader.py deleted file mode 100644 index 4503bc6a79e..00000000000 --- a/src/lerobot/teleoperators/so_leader/so100_leader/config_so100_leader.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python - -# Copyright 2024 The HuggingFace Inc. team. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dataclasses import dataclass - -from ...config import TeleoperatorConfig -from ..so_leader_config_base import SOLeaderTeleopConfigBase - - -@TeleoperatorConfig.register_subclass("so100_leader") -@dataclass -class SO100LeaderConfig(SOLeaderTeleopConfigBase): - pass diff --git a/src/lerobot/teleoperators/so_leader/so100_leader/so100.md b/src/lerobot/teleoperators/so_leader/so100_leader/so100.md deleted file mode 120000 index f06f88ff63e..00000000000 --- a/src/lerobot/teleoperators/so_leader/so100_leader/so100.md +++ /dev/null @@ -1 +0,0 @@ -../../../../../docs/source/so100.mdx \ No newline at end of file diff --git a/src/lerobot/teleoperators/so_leader/so100_leader/so100_leader.py b/src/lerobot/teleoperators/so_leader/so100_leader/so100_leader.py deleted file mode 100644 index 530e4f72344..00000000000 --- a/src/lerobot/teleoperators/so_leader/so100_leader/so100_leader.py +++ /dev/null @@ -1,27 +0,0 @@ -# !/usr/bin/env python - -# Copyright 2026 The HuggingFace Inc. team. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from ..so_leader_base import SOLeaderBase -from .config_so100_leader import SO100LeaderConfig - - -class SO100Leader(SOLeaderBase): - """ - SO-101 leader robot class. [SO-101 Leader Arm](https://github.com/TheRobotStudio/SO-ARM100) designed by TheRobotStudio - """ - - config_class = SO100LeaderConfig - name = "so100_leader" diff --git a/src/lerobot/teleoperators/so_leader/so101.md b/src/lerobot/teleoperators/so_leader/so101.md new file mode 120000 index 00000000000..27b89266029 --- /dev/null +++ b/src/lerobot/teleoperators/so_leader/so101.md @@ -0,0 +1 @@ +../../../../docs/source/so101.mdx \ No newline at end of file diff --git a/src/lerobot/teleoperators/so_leader/so101_leader/config_so101_leader.py b/src/lerobot/teleoperators/so_leader/so101_leader/config_so101_leader.py deleted file mode 100644 index 1e110d73e85..00000000000 --- a/src/lerobot/teleoperators/so_leader/so101_leader/config_so101_leader.py +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env python - -# Copyright 2025 The HuggingFace Inc. team. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from dataclasses import dataclass - -from ...config import TeleoperatorConfig -from ..so_leader_config_base import SOLeaderTeleopConfigBase - - -@TeleoperatorConfig.register_subclass("so101_leader") -@dataclass -class SO101LeaderConfig(SOLeaderTeleopConfigBase): - pass diff --git a/src/lerobot/teleoperators/so_leader/so101_leader/so101.md b/src/lerobot/teleoperators/so_leader/so101_leader/so101.md deleted file mode 120000 index 38f4deca725..00000000000 --- a/src/lerobot/teleoperators/so_leader/so101_leader/so101.md +++ /dev/null @@ -1 +0,0 @@ -../../../../../docs/source/so101.mdx \ No newline at end of file diff --git a/src/lerobot/teleoperators/so_leader/so101_leader/so101_leader.py b/src/lerobot/teleoperators/so_leader/so101_leader/so101_leader.py deleted file mode 100644 index 3aed1f6f9c5..00000000000 --- a/src/lerobot/teleoperators/so_leader/so101_leader/so101_leader.py +++ /dev/null @@ -1,27 +0,0 @@ -# !/usr/bin/env python - -# Copyright 2026 The HuggingFace Inc. team. All rights reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from ..so_leader_base import SOLeaderBase -from .config_so101_leader import SO101LeaderConfig - - -class SO101Leader(SOLeaderBase): - """ - SO-101 leader robot class. [SO-101 Leader Arm](https://github.com/TheRobotStudio/SO-ARM100) designed by TheRobotStudio - """ - - config_class = SO101LeaderConfig - name = "so101_leader" diff --git a/src/lerobot/teleoperators/so_leader/so_leader_base.py b/src/lerobot/teleoperators/so_leader/so_leader.py similarity index 95% rename from src/lerobot/teleoperators/so_leader/so_leader_base.py rename to src/lerobot/teleoperators/so_leader/so_leader.py index 01936d8ac05..760ef2eb125 100644 --- a/src/lerobot/teleoperators/so_leader/so_leader_base.py +++ b/src/lerobot/teleoperators/so_leader/so_leader.py @@ -16,6 +16,7 @@ import logging import time +from typing import TypeAlias from lerobot.motors import Motor, MotorCalibration, MotorNormMode from lerobot.motors.feetech import ( @@ -25,18 +26,18 @@ from lerobot.utils.errors import DeviceAlreadyConnectedError, DeviceNotConnectedError from ..teleoperator import Teleoperator -from .so_leader_config_base import SOLeaderTeleopConfigBase +from .config_so_leader import SOLeaderTeleopConfig logger = logging.getLogger(__name__) -class SOLeaderBase(Teleoperator): +class SOLeader(Teleoperator): """Generic SO leader base for SO-100/101/10X teleoperators.""" - config_class = SOLeaderTeleopConfigBase + config_class = SOLeaderTeleopConfig name = "so_leader" - def __init__(self, config: SOLeaderTeleopConfigBase): + def __init__(self, config: SOLeaderTeleopConfig): super().__init__(config) self.config = config norm_mode_body = MotorNormMode.DEGREES if config.use_degrees else MotorNormMode.RANGE_M100_100 @@ -156,3 +157,7 @@ def disconnect(self) -> None: self.bus.disconnect() logger.info(f"{self} disconnected.") + + +SO100Leader: TypeAlias = SOLeader +SO101Leader: TypeAlias = SOLeader diff --git a/tests/robots/test_so100_follower.py b/tests/robots/test_so100_follower.py index fc300b82028..b61d0ca0147 100644 --- a/tests/robots/test_so100_follower.py +++ b/tests/robots/test_so100_follower.py @@ -66,7 +66,7 @@ def _bus_side_effect(*_args, **kwargs): with ( patch( - "lerobot.robots.so_follower.so_follower_base.FeetechMotorsBus", + "lerobot.robots.so_follower.so_follower.FeetechMotorsBus", side_effect=_bus_side_effect, ), patch.object(SO100Follower, "configure", lambda self: None), From bbf05e3258392611ecc16e86165fc74e4e8e410d Mon Sep 17 00:00:00 2001 From: Steven Palma Date: Mon, 12 Jan 2026 11:50:56 +0100 Subject: [PATCH 5/7] fix(utils): rename bi so --- src/lerobot/robots/utils.py | 2 +- src/lerobot/teleoperators/utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lerobot/robots/utils.py b/src/lerobot/robots/utils.py index 9fa4b6f8517..27abaaa8690 100644 --- a/src/lerobot/robots/utils.py +++ b/src/lerobot/robots/utils.py @@ -53,7 +53,7 @@ def make_robot_from_config(config: RobotConfig) -> Robot: return HopeJrArm(config) elif config.type == "bi_so_follower": - from .so_follower import BiSOFollower + from .bi_so_follower import BiSOFollower return BiSOFollower(config) elif config.type == "reachy2": diff --git a/src/lerobot/teleoperators/utils.py b/src/lerobot/teleoperators/utils.py index 5baae36e2f2..eec2f119c63 100644 --- a/src/lerobot/teleoperators/utils.py +++ b/src/lerobot/teleoperators/utils.py @@ -74,7 +74,7 @@ def make_teleoperator_from_config(config: TeleoperatorConfig) -> Teleoperator: return HomunculusArm(config) elif config.type == "bi_so_leader": - from .so_leader import BiSOLeader + from .bi_so_leader import BiSOLeader return BiSOLeader(config) elif config.type == "reachy2_teleoperator": From c98d6316143524dab53ac7c941620e41240065a4 Mon Sep 17 00:00:00 2001 From: Steven Palma Date: Mon, 12 Jan 2026 12:08:17 +0100 Subject: [PATCH 6/7] fix(scripts): bi so import --- docs/source/envhub_leisaac.mdx | 1 + examples/rtc/eval_with_real_robot.py | 1 + src/lerobot/async_inference/robot_client.py | 1 + src/lerobot/rl/eval_policy.py | 1 + src/lerobot/rl/gym_manipulator.py | 1 + src/lerobot/scripts/lerobot_calibrate.py | 2 ++ src/lerobot/scripts/lerobot_find_joint_limits.py | 2 ++ src/lerobot/scripts/lerobot_record.py | 2 ++ src/lerobot/scripts/lerobot_replay.py | 1 + src/lerobot/scripts/lerobot_setup_motors.py | 2 ++ src/lerobot/scripts/lerobot_teleoperate.py | 2 ++ 11 files changed, 16 insertions(+) diff --git a/docs/source/envhub_leisaac.mdx b/docs/source/envhub_leisaac.mdx index 1fc74c0fa60..2537700a55a 100644 --- a/docs/source/envhub_leisaac.mdx +++ b/docs/source/envhub_leisaac.mdx @@ -138,6 +138,7 @@ from lerobot.teleoperators import ( # noqa: F401 TeleoperatorConfig, make_teleoperator_from_config, so_leader, + bi_so_leader, ) from lerobot.utils.robot_utils import precise_sleep from lerobot.utils.utils import init_logging diff --git a/examples/rtc/eval_with_real_robot.py b/examples/rtc/eval_with_real_robot.py index 991d2468da5..1470899d965 100644 --- a/examples/rtc/eval_with_real_robot.py +++ b/examples/rtc/eval_with_real_robot.py @@ -94,6 +94,7 @@ from lerobot.robots import ( # noqa: F401 Robot, RobotConfig, + bi_so_follower, koch_follower, so_follower, ) diff --git a/src/lerobot/async_inference/robot_client.py b/src/lerobot/async_inference/robot_client.py index e4e3e0abc84..eea5585b03d 100644 --- a/src/lerobot/async_inference/robot_client.py +++ b/src/lerobot/async_inference/robot_client.py @@ -51,6 +51,7 @@ from lerobot.robots import ( # noqa: F401 Robot, RobotConfig, + bi_so_follower, koch_follower, make_robot_from_config, omx_follower, diff --git a/src/lerobot/rl/eval_policy.py b/src/lerobot/rl/eval_policy.py index fb2504f2a6f..6ce24291e27 100644 --- a/src/lerobot/rl/eval_policy.py +++ b/src/lerobot/rl/eval_policy.py @@ -22,6 +22,7 @@ from lerobot.policies.factory import make_policy from lerobot.robots import ( # noqa: F401 RobotConfig, + bi_so_follower, make_robot_from_config, so_follower, ) diff --git a/src/lerobot/rl/gym_manipulator.py b/src/lerobot/rl/gym_manipulator.py index 604adb93126..21561ccf638 100644 --- a/src/lerobot/rl/gym_manipulator.py +++ b/src/lerobot/rl/gym_manipulator.py @@ -54,6 +54,7 @@ from lerobot.processor.converters import identity_transition from lerobot.robots import ( # noqa: F401 RobotConfig, + bi_so_follower, make_robot_from_config, so_follower, ) diff --git a/src/lerobot/scripts/lerobot_calibrate.py b/src/lerobot/scripts/lerobot_calibrate.py index 1468c6e931a..cbc7684d387 100644 --- a/src/lerobot/scripts/lerobot_calibrate.py +++ b/src/lerobot/scripts/lerobot_calibrate.py @@ -36,6 +36,7 @@ from lerobot.robots import ( # noqa: F401 Robot, RobotConfig, + bi_so_follower, hope_jr, koch_follower, lekiwi, @@ -46,6 +47,7 @@ from lerobot.teleoperators import ( # noqa: F401 Teleoperator, TeleoperatorConfig, + bi_so_leader, homunculus, koch_leader, make_teleoperator_from_config, diff --git a/src/lerobot/scripts/lerobot_find_joint_limits.py b/src/lerobot/scripts/lerobot_find_joint_limits.py index b36cdbc9030..20bbc861545 100644 --- a/src/lerobot/scripts/lerobot_find_joint_limits.py +++ b/src/lerobot/scripts/lerobot_find_joint_limits.py @@ -44,6 +44,7 @@ from lerobot.model.kinematics import RobotKinematics from lerobot.robots import ( # noqa: F401 RobotConfig, + bi_so_follower, koch_follower, make_robot_from_config, omx_follower, @@ -51,6 +52,7 @@ ) from lerobot.teleoperators import ( # noqa: F401 TeleoperatorConfig, + bi_so_leader, gamepad, koch_leader, make_teleoperator_from_config, diff --git a/src/lerobot/scripts/lerobot_record.py b/src/lerobot/scripts/lerobot_record.py index 45cec718030..12cbe8f8025 100644 --- a/src/lerobot/scripts/lerobot_record.py +++ b/src/lerobot/scripts/lerobot_record.py @@ -94,6 +94,7 @@ from lerobot.robots import ( # noqa: F401 Robot, RobotConfig, + bi_so_follower, earthrover_mini_plus, hope_jr, koch_follower, @@ -104,6 +105,7 @@ from lerobot.teleoperators import ( # noqa: F401 Teleoperator, TeleoperatorConfig, + bi_so_leader, homunculus, koch_leader, make_teleoperator_from_config, diff --git a/src/lerobot/scripts/lerobot_replay.py b/src/lerobot/scripts/lerobot_replay.py index 7808e2b88fb..8e0d9cf6d89 100644 --- a/src/lerobot/scripts/lerobot_replay.py +++ b/src/lerobot/scripts/lerobot_replay.py @@ -53,6 +53,7 @@ from lerobot.robots import ( # noqa: F401 Robot, RobotConfig, + bi_so_follower, earthrover_mini_plus, hope_jr, koch_follower, diff --git a/src/lerobot/scripts/lerobot_setup_motors.py b/src/lerobot/scripts/lerobot_setup_motors.py index ea5c821d141..01af95b6156 100644 --- a/src/lerobot/scripts/lerobot_setup_motors.py +++ b/src/lerobot/scripts/lerobot_setup_motors.py @@ -30,6 +30,7 @@ from lerobot.robots import ( # noqa: F401 RobotConfig, + bi_so_follower, koch_follower, lekiwi, make_robot_from_config, @@ -38,6 +39,7 @@ ) from lerobot.teleoperators import ( # noqa: F401 TeleoperatorConfig, + bi_so_leader, koch_leader, make_teleoperator_from_config, omx_leader, diff --git a/src/lerobot/scripts/lerobot_teleoperate.py b/src/lerobot/scripts/lerobot_teleoperate.py index 835f1c69285..f2392bb515c 100644 --- a/src/lerobot/scripts/lerobot_teleoperate.py +++ b/src/lerobot/scripts/lerobot_teleoperate.py @@ -70,6 +70,7 @@ from lerobot.robots import ( # noqa: F401 Robot, RobotConfig, + bi_so_follower, earthrover_mini_plus, hope_jr, koch_follower, @@ -80,6 +81,7 @@ from lerobot.teleoperators import ( # noqa: F401 Teleoperator, TeleoperatorConfig, + bi_so_leader, gamepad, homunculus, keyboard, From 751310035e5a26412b26a6f987c555059d154525 Mon Sep 17 00:00:00 2001 From: Steven Palma Date: Mon, 12 Jan 2026 12:26:43 +0100 Subject: [PATCH 7/7] fix(rl): remove imports --- src/lerobot/rl/eval_policy.py | 1 - src/lerobot/rl/gym_manipulator.py | 1 - 2 files changed, 2 deletions(-) diff --git a/src/lerobot/rl/eval_policy.py b/src/lerobot/rl/eval_policy.py index 6ce24291e27..fb2504f2a6f 100644 --- a/src/lerobot/rl/eval_policy.py +++ b/src/lerobot/rl/eval_policy.py @@ -22,7 +22,6 @@ from lerobot.policies.factory import make_policy from lerobot.robots import ( # noqa: F401 RobotConfig, - bi_so_follower, make_robot_from_config, so_follower, ) diff --git a/src/lerobot/rl/gym_manipulator.py b/src/lerobot/rl/gym_manipulator.py index 21561ccf638..604adb93126 100644 --- a/src/lerobot/rl/gym_manipulator.py +++ b/src/lerobot/rl/gym_manipulator.py @@ -54,7 +54,6 @@ from lerobot.processor.converters import identity_transition from lerobot.robots import ( # noqa: F401 RobotConfig, - bi_so_follower, make_robot_from_config, so_follower, )