diff --git a/docs/source/async.mdx b/docs/source/async.mdx index 9dd87472c8..1d3e0edbf1 100644 --- a/docs/source/async.mdx +++ b/docs/source/async.mdx @@ -169,7 +169,7 @@ python -m lerobot.async_inference.robot_client \ ```python import threading -from lerobot.robots.so100_follower import SO100FollowerConfig +from lerobot.robots.so_follower import SO100FollowerConfig from lerobot.cameras.opencv.configuration_opencv import OpenCVCameraConfig from lerobot.async_inference.configs import RobotClientConfig from lerobot.async_inference.robot_client import RobotClient diff --git a/docs/source/envhub_leisaac.mdx b/docs/source/envhub_leisaac.mdx index 5cf4a0e45e..1fc74c0fa6 100644 --- a/docs/source/envhub_leisaac.mdx +++ b/docs/source/envhub_leisaac.mdx @@ -137,7 +137,7 @@ from lerobot.teleoperators import ( # noqa: F401 Teleoperator, TeleoperatorConfig, make_teleoperator_from_config, - so101_leader, + so_leader, ) from lerobot.utils.robot_utils import precise_sleep from lerobot.utils.utils import init_logging @@ -222,7 +222,7 @@ def teleoperate(cfg: TeleoperateConfig): def main(): teleoperate(TeleoperateConfig( - teleop=so101_leader.SO101LeaderConfig( + teleop=so_leader.SO101LeaderConfig( port="/dev/ttyACM0", id='leader', use_degrees=False, diff --git a/docs/source/il_robots.mdx b/docs/source/il_robots.mdx index eb779d2b13..84dc6f2f6a 100644 --- a/docs/source/il_robots.mdx +++ b/docs/source/il_robots.mdx @@ -58,8 +58,8 @@ lerobot-teleoperate \ ```python -from lerobot.teleoperators.so101_leader import SO101LeaderConfig, SO101Leader -from lerobot.robots.so101_follower import SO101FollowerConfig, SO101Follower +from lerobot.teleoperators.so_leader import SO101LeaderConfig, SO101Leader +from lerobot.robots.so_follower import SO101FollowerConfig, SO101Follower robot_config = SO101FollowerConfig( port="/dev/tty.usbmodem58760431541", @@ -195,9 +195,9 @@ lerobot-record \ from lerobot.cameras.opencv.configuration_opencv import OpenCVCameraConfig from lerobot.datasets.lerobot_dataset import LeRobotDataset from lerobot.datasets.utils import hw_to_dataset_features -from lerobot.robots.so100_follower import SO100Follower, SO100FollowerConfig -from lerobot.teleoperators.so100_leader.config_so100_leader import SO100LeaderConfig -from lerobot.teleoperators.so100_leader.so100_leader import SO100Leader +from lerobot.robots.so_follower import SO100Follower, SO100FollowerConfig +from lerobot.teleoperators.so_leader.config_so100_leader import SO100LeaderConfig +from lerobot.teleoperators.so_leader.so100_leader import SO100Leader from lerobot.utils.control_utils import init_keyboard_listener from lerobot.utils.utils import log_say from lerobot.utils.visualization_utils import init_rerun @@ -408,8 +408,8 @@ lerobot-replay \ import time from lerobot.datasets.lerobot_dataset import LeRobotDataset -from lerobot.robots.so100_follower.config_so100_follower import SO100FollowerConfig -from lerobot.robots.so100_follower.so100_follower import SO100Follower +from lerobot.robots.so_follower.config_so100_follower import SO100FollowerConfig +from lerobot.robots.so_follower.so100_follower import SO100Follower from lerobot.utils.robot_utils import precise_sleep from lerobot.utils.utils import log_say @@ -531,8 +531,8 @@ from lerobot.datasets.lerobot_dataset import LeRobotDataset from lerobot.datasets.utils import hw_to_dataset_features from lerobot.policies.act.modeling_act import ACTPolicy from lerobot.policies.factory import make_pre_post_processors -from lerobot.robots.so100_follower.config_so100_follower import SO100FollowerConfig -from lerobot.robots.so100_follower.so100_follower import SO100Follower +from lerobot.robots.so_follower.config_so100_follower import SO100FollowerConfig +from lerobot.robots.so_follower.so100_follower import SO100Follower from lerobot.scripts.lerobot_record import record_loop from lerobot.utils.control_utils import init_keyboard_listener from lerobot.utils.utils import log_say diff --git a/docs/source/integrate_hardware.mdx b/docs/source/integrate_hardware.mdx index e1587be91f..fa36e71708 100644 --- a/docs/source/integrate_hardware.mdx +++ b/docs/source/integrate_hardware.mdx @@ -18,7 +18,7 @@ If you're using Feetech or Dynamixel motors, LeRobot provides built-in bus inter - [`DynamixelMotorsBus`](https://github.com/huggingface/lerobot/blob/main/src/lerobot/motors/dynamixel/dynamixel.py) – for controlling Dynamixel servos Please refer to the [`MotorsBus`](https://github.com/huggingface/lerobot/blob/main/src/lerobot/motors/motors_bus.py) abstract class to learn about its API. -For a good example of how it can be used, you can have a look at our own [SO101 follower implementation](https://github.com/huggingface/lerobot/blob/main/src/lerobot/robots/so101_follower/so101_follower.py) +For a good example of how it can be used, you can have a look at our own [SO101 follower implementation](https://github.com/huggingface/lerobot/blob/main/src/lerobot/robots/so_follower/so101_follower/so101_follower.py) Use these if compatible. Otherwise, you'll need to find or write a Python interface (not covered in this tutorial): diff --git a/docs/source/lekiwi.mdx b/docs/source/lekiwi.mdx index 875394d71d..511521580b 100644 --- a/docs/source/lekiwi.mdx +++ b/docs/source/lekiwi.mdx @@ -204,7 +204,7 @@ lerobot-calibrate \ ```python -from lerobot.teleoperators.so100_leader import SO100LeaderConfig, SO100Leader +from lerobot.teleoperators.so_leader import SO100LeaderConfig, SO100Leader config = SO100LeaderConfig( port="/dev/tty.usbmodem58760431551", diff --git a/docs/source/so100.mdx b/docs/source/so100.mdx index 3c73ae801f..399781ef4b 100644 --- a/docs/source/so100.mdx +++ b/docs/source/so100.mdx @@ -103,7 +103,7 @@ lerobot-setup-motors \ ```python -from lerobot.robots.so100_follower import SO100Follower, SO100FollowerConfig +from lerobot.robots.so_follower import SO100Follower, SO100FollowerConfig config = SO100FollowerConfig( port="/dev/tty.usbmodem585A0076841", @@ -177,7 +177,7 @@ lerobot-setup-motors \ ```python -from lerobot.teleoperators.so100_leader import SO100Leader, SO100LeaderConfig +from lerobot.teleoperators.so_leader import SO100Leader, SO100LeaderConfig config = SO100LeaderConfig( port="/dev/tty.usbmodem585A0076841", @@ -579,7 +579,7 @@ lerobot-calibrate \ ```python -from lerobot.robots.so100_follower import SO100FollowerConfig, SO100Follower +from lerobot.robots.so_follower import SO100FollowerConfig, SO100Follower config = SO100FollowerConfig( port="/dev/tty.usbmodem585A0076891", @@ -617,7 +617,7 @@ lerobot-calibrate \ ```python -from lerobot.teleoperators.so100_leader import SO100LeaderConfig, SO100Leader +from lerobot.teleoperators.so_leader import SO100LeaderConfig, SO100Leader config = SO100LeaderConfig( port="/dev/tty.usbmodem58760431551", diff --git a/docs/source/so101.mdx b/docs/source/so101.mdx index 57e8d691d1..cf882b373b 100644 --- a/docs/source/so101.mdx +++ b/docs/source/so101.mdx @@ -125,7 +125,7 @@ lerobot-setup-motors \ ```python -from lerobot.robots.so101_follower import SO101Follower, SO101FollowerConfig +from lerobot.robots.so_follower import SO101Follower, SO101FollowerConfig config = SO101FollowerConfig( port="/dev/tty.usbmodem585A0076841", @@ -201,7 +201,7 @@ lerobot-setup-motors \ ```python -from lerobot.teleoperators.so101_leader import SO101Leader, SO101LeaderConfig +from lerobot.teleoperators.so_leader import SO101Leader, SO101LeaderConfig config = SO101LeaderConfig( port="/dev/tty.usbmodem585A0076841", @@ -364,7 +364,7 @@ lerobot-calibrate \ ```python -from lerobot.robots.so101_follower import SO101FollowerConfig, SO101Follower +from lerobot.robots.so_follower import SO101FollowerConfig, SO101Follower config = SO101FollowerConfig( port="/dev/tty.usbmodem585A0076891", @@ -413,7 +413,7 @@ lerobot-calibrate \ ```python -from lerobot.teleoperators.so101_leader import SO101LeaderConfig, SO101Leader +from lerobot.teleoperators.so_leader import SO101LeaderConfig, SO101Leader config = SO101LeaderConfig( port="/dev/tty.usbmodem58760431551", diff --git a/examples/backward_compatibility/replay.py b/examples/backward_compatibility/replay.py index 85f3ecef72..ed78d016f8 100644 --- a/examples/backward_compatibility/replay.py +++ b/examples/backward_compatibility/replay.py @@ -41,8 +41,7 @@ RobotConfig, koch_follower, make_robot_from_config, - so100_follower, - so101_follower, + so_follower, ) from lerobot.utils.constants import ACTION from lerobot.utils.robot_utils import precise_sleep diff --git a/examples/lekiwi/record.py b/examples/lekiwi/record.py index 67d826ccb9..18b9f857e1 100644 --- a/examples/lekiwi/record.py +++ b/examples/lekiwi/record.py @@ -21,7 +21,7 @@ from lerobot.robots.lekiwi.lekiwi_client import LeKiwiClient from lerobot.scripts.lerobot_record import record_loop from lerobot.teleoperators.keyboard import KeyboardTeleop, KeyboardTeleopConfig -from lerobot.teleoperators.so100_leader import SO100Leader, SO100LeaderConfig +from lerobot.teleoperators.so_leader import SO100Leader, SO100LeaderConfig from lerobot.utils.constants import ACTION, OBS_STR from lerobot.utils.control_utils import init_keyboard_listener from lerobot.utils.utils import log_say diff --git a/examples/lekiwi/teleoperate.py b/examples/lekiwi/teleoperate.py index c4d20ebbe2..feb3cbb013 100644 --- a/examples/lekiwi/teleoperate.py +++ b/examples/lekiwi/teleoperate.py @@ -18,7 +18,7 @@ from lerobot.robots.lekiwi import LeKiwiClient, LeKiwiClientConfig from lerobot.teleoperators.keyboard.teleop_keyboard import KeyboardTeleop, KeyboardTeleopConfig -from lerobot.teleoperators.so100_leader import SO100Leader, SO100LeaderConfig +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/examples/phone_to_so100/evaluate.py b/examples/phone_to_so100/evaluate.py index 5a47b8ffad..246c923aa1 100644 --- a/examples/phone_to_so100/evaluate.py +++ b/examples/phone_to_so100/evaluate.py @@ -34,12 +34,11 @@ transition_to_observation, transition_to_robot_action, ) -from lerobot.robots.so100_follower.config_so100_follower import SO100FollowerConfig -from lerobot.robots.so100_follower.robot_kinematic_processor import ( +from lerobot.robots.so_follower import SO100Follower, SO100FollowerConfig +from lerobot.robots.so_follower.robot_kinematic_processor import ( ForwardKinematicsJointsToEE, InverseKinematicsEEToJoints, ) -from lerobot.robots.so100_follower.so100_follower import SO100Follower from lerobot.scripts.lerobot_record import record_loop from lerobot.utils.control_utils import init_keyboard_listener from lerobot.utils.utils import log_say diff --git a/examples/phone_to_so100/record.py b/examples/phone_to_so100/record.py index e563d8eb3f..7b5b704e23 100644 --- a/examples/phone_to_so100/record.py +++ b/examples/phone_to_so100/record.py @@ -26,15 +26,14 @@ transition_to_observation, transition_to_robot_action, ) -from lerobot.robots.so100_follower.config_so100_follower import SO100FollowerConfig -from lerobot.robots.so100_follower.robot_kinematic_processor import ( +from lerobot.robots.so_follower import SO100Follower, SO100FollowerConfig +from lerobot.robots.so_follower.robot_kinematic_processor import ( EEBoundsAndSafety, EEReferenceAndDelta, ForwardKinematicsJointsToEE, GripperVelocityToJoint, InverseKinematicsEEToJoints, ) -from lerobot.robots.so100_follower.so100_follower import SO100Follower from lerobot.scripts.lerobot_record import record_loop from lerobot.teleoperators.phone.config_phone import PhoneConfig, PhoneOS from lerobot.teleoperators.phone.phone_processor import MapPhoneActionToRobotAction diff --git a/examples/phone_to_so100/replay.py b/examples/phone_to_so100/replay.py index ab6ce3cede..875025dfc9 100644 --- a/examples/phone_to_so100/replay.py +++ b/examples/phone_to_so100/replay.py @@ -23,11 +23,10 @@ robot_action_observation_to_transition, transition_to_robot_action, ) -from lerobot.robots.so100_follower.config_so100_follower import SO100FollowerConfig -from lerobot.robots.so100_follower.robot_kinematic_processor import ( +from lerobot.robots.so_follower import SO100Follower, SO100FollowerConfig +from lerobot.robots.so_follower.robot_kinematic_processor import ( InverseKinematicsEEToJoints, ) -from lerobot.robots.so100_follower.so100_follower import SO100Follower from lerobot.utils.constants import ACTION from lerobot.utils.robot_utils import precise_sleep from lerobot.utils.utils import log_say diff --git a/examples/phone_to_so100/teleoperate.py b/examples/phone_to_so100/teleoperate.py index 2ac8b3cced..6eaaec8061 100644 --- a/examples/phone_to_so100/teleoperate.py +++ b/examples/phone_to_so100/teleoperate.py @@ -21,14 +21,13 @@ robot_action_observation_to_transition, transition_to_robot_action, ) -from lerobot.robots.so100_follower.config_so100_follower import SO100FollowerConfig -from lerobot.robots.so100_follower.robot_kinematic_processor import ( +from lerobot.robots.so_follower import SO100Follower, SO100FollowerConfig +from lerobot.robots.so_follower.robot_kinematic_processor import ( EEBoundsAndSafety, EEReferenceAndDelta, GripperVelocityToJoint, InverseKinematicsEEToJoints, ) -from lerobot.robots.so100_follower.so100_follower import SO100Follower from lerobot.teleoperators.phone.config_phone import PhoneConfig, PhoneOS from lerobot.teleoperators.phone.phone_processor import MapPhoneActionToRobotAction from lerobot.teleoperators.phone.teleop_phone import Phone diff --git a/examples/rtc/eval_with_real_robot.py b/examples/rtc/eval_with_real_robot.py index 5f44649dab..991d2468da 100644 --- a/examples/rtc/eval_with_real_robot.py +++ b/examples/rtc/eval_with_real_robot.py @@ -95,8 +95,7 @@ Robot, RobotConfig, koch_follower, - so100_follower, - so101_follower, + so_follower, ) from lerobot.robots.utils import make_robot_from_config from lerobot.utils.constants import OBS_IMAGES diff --git a/examples/so100_to_so100_EE/evaluate.py b/examples/so100_to_so100_EE/evaluate.py index 90973d373f..87d188f994 100644 --- a/examples/so100_to_so100_EE/evaluate.py +++ b/examples/so100_to_so100_EE/evaluate.py @@ -34,12 +34,11 @@ transition_to_observation, transition_to_robot_action, ) -from lerobot.robots.so100_follower.config_so100_follower import SO100FollowerConfig -from lerobot.robots.so100_follower.robot_kinematic_processor import ( +from lerobot.robots.so_follower import SO100Follower, SO100FollowerConfig +from lerobot.robots.so_follower.robot_kinematic_processor import ( ForwardKinematicsJointsToEE, InverseKinematicsEEToJoints, ) -from lerobot.robots.so100_follower.so100_follower import SO100Follower from lerobot.scripts.lerobot_record import record_loop from lerobot.utils.control_utils import init_keyboard_listener from lerobot.utils.utils import log_say diff --git a/examples/so100_to_so100_EE/record.py b/examples/so100_to_so100_EE/record.py index 6bfdfe32df..db24f4b93a 100644 --- a/examples/so100_to_so100_EE/record.py +++ b/examples/so100_to_so100_EE/record.py @@ -27,16 +27,15 @@ transition_to_observation, transition_to_robot_action, ) -from lerobot.robots.so100_follower.config_so100_follower import SO100FollowerConfig -from lerobot.robots.so100_follower.robot_kinematic_processor import ( +from lerobot.robots.so_follower import SO100Follower, SO100FollowerConfig +from lerobot.robots.so_follower.robot_kinematic_processor import ( EEBoundsAndSafety, ForwardKinematicsJointsToEE, InverseKinematicsEEToJoints, ) -from lerobot.robots.so100_follower.so100_follower import SO100Follower from lerobot.scripts.lerobot_record import record_loop -from lerobot.teleoperators.so100_leader.config_so100_leader import SO100LeaderConfig -from lerobot.teleoperators.so100_leader.so100_leader import SO100Leader +from lerobot.teleoperators.so_leader import SO100LeaderConfig +from lerobot.teleoperators.so_leader.so100_leader import SO100Leader 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/replay.py b/examples/so100_to_so100_EE/replay.py index 59c524078c..7d35a7b440 100644 --- a/examples/so100_to_so100_EE/replay.py +++ b/examples/so100_to_so100_EE/replay.py @@ -24,11 +24,10 @@ robot_action_observation_to_transition, transition_to_robot_action, ) -from lerobot.robots.so100_follower.config_so100_follower import SO100FollowerConfig -from lerobot.robots.so100_follower.robot_kinematic_processor import ( +from lerobot.robots.so_follower import SO100Follower, SO100FollowerConfig +from lerobot.robots.so_follower.robot_kinematic_processor import ( InverseKinematicsEEToJoints, ) -from lerobot.robots.so100_follower.so100_follower import SO100Follower from lerobot.utils.constants import ACTION from lerobot.utils.robot_utils import precise_sleep from lerobot.utils.utils import log_say diff --git a/examples/so100_to_so100_EE/teleoperate.py b/examples/so100_to_so100_EE/teleoperate.py index 21299103b2..d520a6eaf6 100644 --- a/examples/so100_to_so100_EE/teleoperate.py +++ b/examples/so100_to_so100_EE/teleoperate.py @@ -23,15 +23,14 @@ robot_action_to_transition, transition_to_robot_action, ) -from lerobot.robots.so100_follower.config_so100_follower import SO100FollowerConfig -from lerobot.robots.so100_follower.robot_kinematic_processor import ( +from lerobot.robots.so_follower import SO100Follower, SO100FollowerConfig +from lerobot.robots.so_follower.robot_kinematic_processor import ( EEBoundsAndSafety, ForwardKinematicsJointsToEE, InverseKinematicsEEToJoints, ) -from lerobot.robots.so100_follower.so100_follower import SO100Follower -from lerobot.teleoperators.so100_leader.config_so100_leader import SO100LeaderConfig -from lerobot.teleoperators.so100_leader.so100_leader import SO100Leader +from lerobot.teleoperators.so_leader import SO100LeaderConfig +from lerobot.teleoperators.so_leader.so100_leader import SO100Leader from lerobot.utils.robot_utils import precise_sleep from lerobot.utils.visualization_utils import init_rerun, log_rerun_data diff --git a/examples/tutorial/act/act_using_example.py b/examples/tutorial/act/act_using_example.py index b268e87903..60bc802d8c 100644 --- a/examples/tutorial/act/act_using_example.py +++ b/examples/tutorial/act/act_using_example.py @@ -5,8 +5,7 @@ from lerobot.policies.act.modeling_act import ACTPolicy from lerobot.policies.factory import make_pre_post_processors from lerobot.policies.utils import build_inference_frame, make_robot_action -from lerobot.robots.so100_follower.config_so100_follower import SO100FollowerConfig -from lerobot.robots.so100_follower.so100_follower import SO100Follower +from lerobot.robots.so_follower import SO100Follower, SO100FollowerConfig MAX_EPISODES = 5 MAX_STEPS_PER_EPISODE = 20 diff --git a/examples/tutorial/async-inf/robot_client.py b/examples/tutorial/async-inf/robot_client.py index fff7b15b3b..eb37511697 100644 --- a/examples/tutorial/async-inf/robot_client.py +++ b/examples/tutorial/async-inf/robot_client.py @@ -4,7 +4,7 @@ from lerobot.async_inference.helpers import visualize_action_queue_size from lerobot.async_inference.robot_client import RobotClient from lerobot.cameras.opencv.configuration_opencv import OpenCVCameraConfig -from lerobot.robots.so100_follower import SO100FollowerConfig +from lerobot.robots.so_follower import SO100FollowerConfig def main(): diff --git a/examples/tutorial/diffusion/diffusion_using_example.py b/examples/tutorial/diffusion/diffusion_using_example.py index 96cc607b67..d8ac75cfee 100644 --- a/examples/tutorial/diffusion/diffusion_using_example.py +++ b/examples/tutorial/diffusion/diffusion_using_example.py @@ -5,8 +5,7 @@ from lerobot.policies.diffusion.modeling_diffusion import DiffusionPolicy from lerobot.policies.factory import make_pre_post_processors from lerobot.policies.utils import build_inference_frame, make_robot_action -from lerobot.robots.so100_follower.config_so100_follower import SO100FollowerConfig -from lerobot.robots.so100_follower.so100_follower import SO100Follower +from lerobot.robots.so_follower import SO100Follower, SO100FollowerConfig MAX_EPISODES = 5 MAX_STEPS_PER_EPISODE = 20 diff --git a/examples/tutorial/pi0/using_pi0_example.py b/examples/tutorial/pi0/using_pi0_example.py index 362092ccfe..056c3d81a6 100644 --- a/examples/tutorial/pi0/using_pi0_example.py +++ b/examples/tutorial/pi0/using_pi0_example.py @@ -5,8 +5,7 @@ from lerobot.policies.factory import make_pre_post_processors from lerobot.policies.pi0.modeling_pi0 import PI0Policy from lerobot.policies.utils import build_inference_frame, make_robot_action -from lerobot.robots.so100_follower.config_so100_follower import SO100FollowerConfig -from lerobot.robots.so100_follower.so100_follower import SO100Follower +from lerobot.robots.so_follower import SO100Follower, SO100FollowerConfig MAX_EPISODES = 5 MAX_STEPS_PER_EPISODE = 20 diff --git a/examples/tutorial/rl/hilserl_example.py b/examples/tutorial/rl/hilserl_example.py index c49233ebb1..980ac79851 100644 --- a/examples/tutorial/rl/hilserl_example.py +++ b/examples/tutorial/rl/hilserl_example.py @@ -14,8 +14,8 @@ from lerobot.policies.sac.reward_model.modeling_classifier import Classifier from lerobot.rl.buffer import ReplayBuffer from lerobot.rl.gym_manipulator import make_robot_env -from lerobot.robots.so100_follower import SO100FollowerConfig -from lerobot.teleoperators.so100_leader import SO100LeaderConfig +from lerobot.robots.so_follower import SO100FollowerConfig +from lerobot.teleoperators.so_leader import SO100LeaderConfig from lerobot.teleoperators.utils import TeleopEvents LOG_EVERY = 10 diff --git a/examples/tutorial/smolvla/using_smolvla_example.py b/examples/tutorial/smolvla/using_smolvla_example.py index d4219f3166..ce3aa7bcaa 100644 --- a/examples/tutorial/smolvla/using_smolvla_example.py +++ b/examples/tutorial/smolvla/using_smolvla_example.py @@ -5,8 +5,7 @@ from lerobot.policies.factory import make_pre_post_processors from lerobot.policies.smolvla.modeling_smolvla import SmolVLAPolicy from lerobot.policies.utils import build_inference_frame, make_robot_action -from lerobot.robots.so100_follower.config_so100_follower import SO100FollowerConfig -from lerobot.robots.so100_follower.so100_follower import SO100Follower +from lerobot.robots.so_follower import SO100Follower, SO100FollowerConfig MAX_EPISODES = 5 MAX_STEPS_PER_EPISODE = 20 diff --git a/src/lerobot/async_inference/robot_client.py b/src/lerobot/async_inference/robot_client.py index d32aa6a216..c3668d40bd 100644 --- a/src/lerobot/async_inference/robot_client.py +++ b/src/lerobot/async_inference/robot_client.py @@ -55,8 +55,7 @@ koch_follower, make_robot_from_config, omx_follower, - so100_follower, - so101_follower, + so_follower, ) from lerobot.transport import ( services_pb2, # type: ignore diff --git a/src/lerobot/rl/actor.py b/src/lerobot/rl/actor.py index 641a21d037..7427633d2d 100644 --- a/src/lerobot/rl/actor.py +++ b/src/lerobot/rl/actor.py @@ -65,8 +65,8 @@ from lerobot.processor import TransitionKey from lerobot.rl.process import ProcessSignalHandler from lerobot.rl.queue import get_last_item_from_queue -from lerobot.robots import so100_follower # noqa: F401 -from lerobot.teleoperators import gamepad, so101_leader # noqa: F401 +from lerobot.robots import so_follower # noqa: F401 +from lerobot.teleoperators import gamepad, so_leader # noqa: F401 from lerobot.teleoperators.utils import TeleopEvents from lerobot.transport import services_pb2, services_pb2_grpc from lerobot.transport.utils import ( diff --git a/src/lerobot/rl/eval_policy.py b/src/lerobot/rl/eval_policy.py index 16bb64a73c..fb2504f2a6 100644 --- a/src/lerobot/rl/eval_policy.py +++ b/src/lerobot/rl/eval_policy.py @@ -23,11 +23,11 @@ from lerobot.robots import ( # noqa: F401 RobotConfig, make_robot_from_config, - so100_follower, + so_follower, ) from lerobot.teleoperators import ( gamepad, # noqa: F401 - so101_leader, # noqa: F401 + so_leader, # noqa: F401 ) from .gym_manipulator import make_robot_env diff --git a/src/lerobot/rl/gym_manipulator.py b/src/lerobot/rl/gym_manipulator.py index 7bc74a959e..604adb9312 100644 --- a/src/lerobot/rl/gym_manipulator.py +++ b/src/lerobot/rl/gym_manipulator.py @@ -55,10 +55,10 @@ from lerobot.robots import ( # noqa: F401 RobotConfig, make_robot_from_config, - so100_follower, + so_follower, ) from lerobot.robots.robot import Robot -from lerobot.robots.so100_follower.robot_kinematic_processor import ( +from lerobot.robots.so_follower.robot_kinematic_processor import ( EEBoundsAndSafety, EEReferenceAndDelta, ForwardKinematicsJointsToEEObservation, @@ -69,7 +69,7 @@ gamepad, # noqa: F401 keyboard, # noqa: F401 make_teleoperator_from_config, - so101_leader, # noqa: F401 + so_leader, # noqa: F401 ) from lerobot.teleoperators.teleoperator import Teleoperator from lerobot.teleoperators.utils import TeleopEvents diff --git a/src/lerobot/rl/learner.py b/src/lerobot/rl/learner.py index d9758d3a35..abc5c95049 100644 --- a/src/lerobot/rl/learner.py +++ b/src/lerobot/rl/learner.py @@ -69,8 +69,8 @@ from lerobot.rl.buffer import ReplayBuffer, concatenate_batch_transitions from lerobot.rl.process import ProcessSignalHandler from lerobot.rl.wandb_utils import WandBLogger -from lerobot.robots import so100_follower # noqa: F401 -from lerobot.teleoperators import gamepad, so101_leader # noqa: F401 +from lerobot.robots import so_follower # noqa: F401 +from lerobot.teleoperators import gamepad, so_leader # noqa: F401 from lerobot.teleoperators.utils import TeleopEvents from lerobot.transport import services_pb2_grpc from lerobot.transport.utils import ( diff --git a/src/lerobot/robots/bi_so100_follower/bi_so100_follower.py b/src/lerobot/robots/bi_so100_follower/bi_so100_follower.py index 7992b79fd4..87a7edcc5d 100644 --- a/src/lerobot/robots/bi_so100_follower/bi_so100_follower.py +++ b/src/lerobot/robots/bi_so100_follower/bi_so100_follower.py @@ -20,8 +20,7 @@ from typing import Any from lerobot.cameras.utils import make_cameras_from_configs -from lerobot.robots.so100_follower import SO100Follower -from lerobot.robots.so100_follower.config_so100_follower import SO100FollowerConfig +from lerobot.robots.so_follower import SO100Follower, SO100FollowerConfig from ..robot import Robot from .config_bi_so100_follower import BiSO100FollowerConfig diff --git a/src/lerobot/robots/so100_follower/config_so100_follower.py b/src/lerobot/robots/so100_follower/config_so100_follower.py deleted file mode 100644 index 272b8c43f4..0000000000 --- a/src/lerobot/robots/so100_follower/config_so100_follower.py +++ /dev/null @@ -1,41 +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("so100_follower") -@dataclass -class SO100FollowerConfig(RobotConfig): - # Port to connect to the arm - port: str - - disable_torque_on_disconnect: bool = True - - # `max_relative_target` limits the magnitude of the relative positional target vector for safety purposes. - # Set this to a positive scalar to have the same value for all motors, or a dictionary that maps motor - # names to the max_relative_target value for that motor. - max_relative_target: float | dict[str, float] | None = None - - # cameras - cameras: dict[str, CameraConfig] = field(default_factory=dict) - - # Set to `True` for backward compatibility with previous policies/dataset - use_degrees: bool = False diff --git a/src/lerobot/robots/so100_follower/so100.mdx b/src/lerobot/robots/so100_follower/so100.mdx deleted file mode 120000 index ad1154e75a..0000000000 --- a/src/lerobot/robots/so100_follower/so100.mdx +++ /dev/null @@ -1 +0,0 @@ -../../../../docs/source/so100.mdx \ No newline at end of file diff --git a/src/lerobot/robots/so101_follower/so101.mdx b/src/lerobot/robots/so101_follower/so101.mdx deleted file mode 120000 index 27b8926602..0000000000 --- a/src/lerobot/robots/so101_follower/so101.mdx +++ /dev/null @@ -1 +0,0 @@ -../../../../docs/source/so101.mdx \ No newline at end of file diff --git a/src/lerobot/robots/so101_follower/so101_follower.py b/src/lerobot/robots/so101_follower/so101_follower.py deleted file mode 100644 index acfd4bd114..0000000000 --- a/src/lerobot/robots/so101_follower/so101_follower.py +++ /dev/null @@ -1,230 +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.motors import Motor, MotorCalibration, MotorNormMode -from lerobot.motors.feetech import ( - FeetechMotorsBus, - OperatingMode, -) -from lerobot.utils.errors import DeviceAlreadyConnectedError, DeviceNotConnectedError - -from ..robot import Robot -from ..utils import ensure_safe_goal_position -from .config_so101_follower import SO101FollowerConfig - -logger = logging.getLogger(__name__) - - -class SO101Follower(Robot): - """ - SO-101 Follower Arm designed by TheRobotStudio and Hugging Face. - """ - - config_class = SO101FollowerConfig - name = "so101_follower" - - def __init__(self, config: SO101FollowerConfig): - super().__init__(config) - self.config = config - norm_mode_body = MotorNormMode.DEGREES if config.use_degrees else MotorNormMode.RANGE_M100_100 - self.bus = FeetechMotorsBus( - port=self.config.port, - motors={ - "shoulder_pan": Motor(1, "sts3215", norm_mode_body), - "shoulder_lift": Motor(2, "sts3215", norm_mode_body), - "elbow_flex": Motor(3, "sts3215", norm_mode_body), - "wrist_flex": Motor(4, "sts3215", norm_mode_body), - "wrist_roll": Motor(5, "sts3215", norm_mode_body), - "gripper": Motor(6, "sts3215", MotorNormMode.RANGE_0_100), - }, - calibration=self.calibration, - ) - self.cameras = make_cameras_from_configs(config.cameras) - - @property - def _motors_ft(self) -> dict[str, type]: - return {f"{motor}.pos": float for motor in self.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.bus.is_connected and all(cam.is_connected for cam in self.cameras.values()) - - def connect(self, calibrate: bool = True) -> None: - """ - We assume that at connection time, arm is in a rest position, - and torque can be safely disabled to run calibration. - """ - if self.is_connected: - raise DeviceAlreadyConnectedError(f"{self} already connected") - - self.bus.connect() - if not self.is_calibrated and calibrate: - logger.info( - "Mismatch between calibration values in the motor and the calibration file or no calibration file found" - ) - self.calibrate() - - for cam in self.cameras.values(): - cam.connect() - - self.configure() - logger.info(f"{self} connected.") - - @property - def is_calibrated(self) -> bool: - return self.bus.is_calibrated - - def calibrate(self) -> None: - if self.calibration: - # self.calibration is not empty here - user_input = input( - f"Press ENTER to use provided calibration file associated with the id {self.id}, or type 'c' and press ENTER to run calibration: " - ) - if user_input.strip().lower() != "c": - logger.info(f"Writing calibration file associated with the id {self.id} to the motors") - self.bus.write_calibration(self.calibration) - return - - logger.info(f"\nRunning calibration of {self}") - self.bus.disable_torque() - for motor in self.bus.motors: - self.bus.write("Operating_Mode", motor, OperatingMode.POSITION.value) - - input(f"Move {self} to the middle of its range of motion and press ENTER....") - homing_offsets = self.bus.set_half_turn_homings() - - print( - "Move all joints sequentially through their entire ranges " - "of motion.\nRecording positions. Press ENTER to stop..." - ) - range_mins, range_maxes = self.bus.record_ranges_of_motion() - - self.calibration = {} - for motor, m in self.bus.motors.items(): - self.calibration[motor] = MotorCalibration( - id=m.id, - drive_mode=0, - homing_offset=homing_offsets[motor], - range_min=range_mins[motor], - range_max=range_maxes[motor], - ) - - self.bus.write_calibration(self.calibration) - self._save_calibration() - print("Calibration saved to", self.calibration_fpath) - - def configure(self) -> None: - with self.bus.torque_disabled(): - self.bus.configure_motors() - for motor in self.bus.motors: - self.bus.write("Operating_Mode", motor, OperatingMode.POSITION.value) - # Set P_Coefficient to lower value to avoid shakiness (Default is 32) - self.bus.write("P_Coefficient", motor, 16) - # Set I_Coefficient and D_Coefficient to default value 0 and 32 - self.bus.write("I_Coefficient", motor, 0) - self.bus.write("D_Coefficient", motor, 32) - - if motor == "gripper": - self.bus.write( - "Max_Torque_Limit", motor, 500 - ) # 50% of the max torque limit to avoid burnout - self.bus.write("Protection_Current", motor, 250) # 50% of max current to avoid burnout - self.bus.write("Overload_Torque", motor, 25) # 25% torque when overloaded - - def setup_motors(self) -> None: - for motor in reversed(self.bus.motors): - input(f"Connect the controller board to the '{motor}' motor only and press enter.") - self.bus.setup_motor(motor) - print(f"'{motor}' motor id set to {self.bus.motors[motor].id}") - - def get_observation(self) -> dict[str, Any]: - if not self.is_connected: - raise DeviceNotConnectedError(f"{self} is not connected.") - - # Read arm position - start = time.perf_counter() - obs_dict = self.bus.sync_read("Present_Position") - obs_dict = {f"{motor}.pos": val for motor, val in obs_dict.items()} - dt_ms = (time.perf_counter() - start) * 1e3 - logger.debug(f"{self} read state: {dt_ms:.1f}ms") - - # Capture images from cameras - 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]: - """Command arm to move to a target joint configuration. - - The relative action magnitude may be clipped depending on the configuration parameter - `max_relative_target`. In this case, the action sent differs from original action. - Thus, this function always returns the action actually sent. - - Raises: - RobotDeviceNotConnectedError: if robot is not connected. - - Returns: - the action sent to the motors, potentially clipped. - """ - if not self.is_connected: - raise DeviceNotConnectedError(f"{self} is not connected.") - - goal_pos = {key.removesuffix(".pos"): val for key, val in action.items() if key.endswith(".pos")} - - # Cap goal position when too far away from present position. - # /!\ Slower fps expected due to reading from the follower. - if self.config.max_relative_target is not None: - present_pos = self.bus.sync_read("Present_Position") - goal_present_pos = {key: (g_pos, present_pos[key]) for key, g_pos in goal_pos.items()} - goal_pos = ensure_safe_goal_position(goal_present_pos, self.config.max_relative_target) - - # Send goal position to the arm - self.bus.sync_write("Goal_Position", goal_pos) - return {f"{motor}.pos": val for motor, val in goal_pos.items()} - - def disconnect(self): - if not self.is_connected: - raise DeviceNotConnectedError(f"{self} is not connected.") - - self.bus.disconnect(self.config.disable_torque_on_disconnect) - for cam in self.cameras.values(): - cam.disconnect() - - logger.info(f"{self} disconnected.") diff --git a/src/lerobot/robots/so_follower/__init__.py b/src/lerobot/robots/so_follower/__init__.py new file mode 100644 index 0000000000..82755250cd --- /dev/null +++ b/src/lerobot/robots/so_follower/__init__.py @@ -0,0 +1,23 @@ +#!/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 .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/so100_follower/robot_kinematic_processor.py b/src/lerobot/robots/so_follower/robot_kinematic_processor.py similarity index 100% rename from src/lerobot/robots/so100_follower/robot_kinematic_processor.py rename to src/lerobot/robots/so_follower/robot_kinematic_processor.py diff --git a/src/lerobot/teleoperators/so101_leader/__init__.py b/src/lerobot/robots/so_follower/so100_follower/config_so100_follower.py similarity index 64% rename from src/lerobot/teleoperators/so101_leader/__init__.py rename to src/lerobot/robots/so_follower/so100_follower/config_so100_follower.py index 11e277c915..3e8dd7e9ff 100644 --- a/src/lerobot/teleoperators/so101_leader/__init__.py +++ b/src/lerobot/robots/so_follower/so100_follower/config_so100_follower.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -# Copyright 2025 The HuggingFace Inc. team. All rights reserved. +# 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. @@ -14,5 +14,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .config_so101_leader import SO101LeaderConfig -from .so101_leader import SO101Leader +from dataclasses import dataclass + +from ...config import RobotConfig +from ..so_follower_config_base import SOFollowerConfigBase + + +@RobotConfig.register_subclass("so100_follower") +@dataclass +class SO100FollowerConfig(SOFollowerConfigBase): + pass diff --git a/src/lerobot/robots/so_follower/so100_follower/so100.md b/src/lerobot/robots/so_follower/so100_follower/so100.md new file mode 120000 index 0000000000..f06f88ff63 --- /dev/null +++ b/src/lerobot/robots/so_follower/so100_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_follower.py b/src/lerobot/robots/so_follower/so100_follower/so100_follower.py new file mode 100644 index 0000000000..ef61f5ce38 --- /dev/null +++ b/src/lerobot/robots/so_follower/so100_follower/so100_follower.py @@ -0,0 +1,27 @@ +#!/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/teleoperators/so100_leader/__init__.py b/src/lerobot/robots/so_follower/so101_follower/config_so101_follower.py similarity index 64% rename from src/lerobot/teleoperators/so100_leader/__init__.py rename to src/lerobot/robots/so_follower/so101_follower/config_so101_follower.py index 747416be2e..950e8c839c 100644 --- a/src/lerobot/teleoperators/so100_leader/__init__.py +++ b/src/lerobot/robots/so_follower/so101_follower/config_so101_follower.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -# Copyright 2025 The HuggingFace Inc. team. All rights reserved. +# 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. @@ -14,5 +14,13 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .config_so100_leader import SO100LeaderConfig -from .so100_leader import SO100Leader +from dataclasses import dataclass + +from ...config import RobotConfig +from ..so_follower_config_base import SOFollowerConfigBase + + +@RobotConfig.register_subclass("so101_follower") +@dataclass +class SO101FollowerConfig(SOFollowerConfigBase): + pass diff --git a/src/lerobot/robots/so_follower/so101_follower/so101.md b/src/lerobot/robots/so_follower/so101_follower/so101.md new file mode 120000 index 0000000000..38f4deca72 --- /dev/null +++ b/src/lerobot/robots/so_follower/so101_follower/so101.md @@ -0,0 +1 @@ +../../../../../docs/source/so101.mdx \ No newline at end of file diff --git a/src/lerobot/robots/so101_follower/__init__.py b/src/lerobot/robots/so_follower/so101_follower/so101_follower.py similarity index 63% rename from src/lerobot/robots/so101_follower/__init__.py rename to src/lerobot/robots/so_follower/so101_follower/so101_follower.py index 9ff2baf452..b4cfb2711d 100644 --- a/src/lerobot/robots/so101_follower/__init__.py +++ b/src/lerobot/robots/so_follower/so101_follower/so101_follower.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -# Copyright 2025 The HuggingFace Inc. team. All rights reserved. +# 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. @@ -14,5 +14,14 @@ # 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 -from .so101_follower import SO101Follower + + +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/so100_follower/so100_follower.py b/src/lerobot/robots/so_follower/so_follower_base.py similarity index 93% rename from src/lerobot/robots/so100_follower/so100_follower.py rename to src/lerobot/robots/so_follower/so_follower_base.py index d660ebed43..5e6e328882 100644 --- a/src/lerobot/robots/so100_follower/so100_follower.py +++ b/src/lerobot/robots/so_follower/so_follower_base.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -# Copyright 2024 The HuggingFace Inc. team. All rights reserved. +# 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. @@ -29,22 +29,23 @@ from ..robot import Robot from ..utils import ensure_safe_goal_position -from .config_so100_follower import SO100FollowerConfig +from .so_follower_config_base import SOFollowerConfigBase logger = logging.getLogger(__name__) -class SO100Follower(Robot): +class SOFollowerBase(Robot): """ - [SO-100 Follower Arm](https://github.com/TheRobotStudio/SO-ARM100) designed by TheRobotStudio + 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 = SO100FollowerConfig - name = "so100_follower" + # `config_class` and `name` should be set by subclasses - def __init__(self, config: SO100FollowerConfig): + def __init__(self, config: SOFollowerConfigBase): super().__init__(config) self.config = config + # choose normalization mode depending on config if available norm_mode_body = MotorNormMode.DEGREES if config.use_degrees else MotorNormMode.RANGE_M100_100 self.bus = FeetechMotorsBus( port=self.config.port, @@ -126,6 +127,7 @@ def calibrate(self) -> None: input(f"Move {self} to the middle of its range of motion and press ENTER....") homing_offsets = self.bus.set_half_turn_homings() + # Attempt to call record_ranges_of_motion with a reduced motor set when appropriate. full_turn_motor = "wrist_roll" unknown_range_motors = [motor for motor in self.bus.motors if motor != full_turn_motor] print( diff --git a/src/lerobot/robots/so101_follower/config_so101_follower.py b/src/lerobot/robots/so_follower/so_follower_config_base.py similarity index 93% rename from src/lerobot/robots/so101_follower/config_so101_follower.py rename to src/lerobot/robots/so_follower/so_follower_config_base.py index 03c3530c2f..6e42df2787 100644 --- a/src/lerobot/robots/so101_follower/config_so101_follower.py +++ b/src/lerobot/robots/so_follower/so_follower_config_base.py @@ -21,9 +21,10 @@ from ..config import RobotConfig -@RobotConfig.register_subclass("so101_follower") @dataclass -class SO101FollowerConfig(RobotConfig): +class SOFollowerConfigBase(RobotConfig): + """Base configuration class for SO Follower robots.""" + # Port to connect to the arm port: str diff --git a/src/lerobot/robots/utils.py b/src/lerobot/robots/utils.py index 9c5043335a..ad6cc3da1e 100644 --- a/src/lerobot/robots/utils.py +++ b/src/lerobot/robots/utils.py @@ -33,11 +33,11 @@ def make_robot_from_config(config: RobotConfig) -> Robot: return OmxFollower(config) elif config.type == "so100_follower": - from .so100_follower import SO100Follower + from .so_follower import SO100Follower return SO100Follower(config) elif config.type == "so101_follower": - from .so101_follower import SO101Follower + from .so_follower import SO101Follower return SO101Follower(config) elif config.type == "lekiwi": diff --git a/src/lerobot/scripts/lerobot_calibrate.py b/src/lerobot/scripts/lerobot_calibrate.py index 910a9a1b56..1468c6e931 100644 --- a/src/lerobot/scripts/lerobot_calibrate.py +++ b/src/lerobot/scripts/lerobot_calibrate.py @@ -41,8 +41,7 @@ lekiwi, make_robot_from_config, omx_follower, - so100_follower, - so101_follower, + so_follower, ) from lerobot.teleoperators import ( # noqa: F401 Teleoperator, @@ -51,8 +50,7 @@ koch_leader, make_teleoperator_from_config, omx_leader, - so100_leader, - so101_leader, + so_leader, ) from lerobot.utils.import_utils import register_third_party_plugins from lerobot.utils.utils import init_logging diff --git a/src/lerobot/scripts/lerobot_find_joint_limits.py b/src/lerobot/scripts/lerobot_find_joint_limits.py index f97c0d8209..b36cdbc903 100644 --- a/src/lerobot/scripts/lerobot_find_joint_limits.py +++ b/src/lerobot/scripts/lerobot_find_joint_limits.py @@ -47,8 +47,7 @@ koch_follower, make_robot_from_config, omx_follower, - so100_follower, - so101_follower, + so_follower, ) from lerobot.teleoperators import ( # noqa: F401 TeleoperatorConfig, @@ -56,8 +55,7 @@ koch_leader, make_teleoperator_from_config, omx_leader, - so100_leader, - so101_leader, + so_leader, ) from lerobot.utils.robot_utils import precise_sleep diff --git a/src/lerobot/scripts/lerobot_record.py b/src/lerobot/scripts/lerobot_record.py index 62bf216537..fb01389a9e 100644 --- a/src/lerobot/scripts/lerobot_record.py +++ b/src/lerobot/scripts/lerobot_record.py @@ -98,8 +98,7 @@ koch_follower, make_robot_from_config, omx_follower, - so100_follower, - so101_follower, + so_follower, ) from lerobot.teleoperators import ( # noqa: F401 Teleoperator, @@ -109,8 +108,7 @@ koch_leader, make_teleoperator_from_config, omx_leader, - so100_leader, - so101_leader, + so_leader, ) from lerobot.teleoperators.keyboard.teleop_keyboard import KeyboardTeleop from lerobot.utils.constants import ACTION, OBS_STR @@ -275,8 +273,8 @@ def record_loop( if isinstance( t, ( - so100_leader.SO100Leader - | so101_leader.SO101Leader + so_leader.SO100Leader + | so_leader.SO101Leader | koch_leader.KochLeader | omx_leader.OmxLeader ), diff --git a/src/lerobot/scripts/lerobot_replay.py b/src/lerobot/scripts/lerobot_replay.py index 5cde4251c9..af7c633651 100644 --- a/src/lerobot/scripts/lerobot_replay.py +++ b/src/lerobot/scripts/lerobot_replay.py @@ -59,8 +59,7 @@ koch_follower, make_robot_from_config, omx_follower, - so100_follower, - so101_follower, + so_follower, ) from lerobot.utils.constants import ACTION from lerobot.utils.import_utils import register_third_party_plugins diff --git a/src/lerobot/scripts/lerobot_setup_motors.py b/src/lerobot/scripts/lerobot_setup_motors.py index b721e55ca1..ea5c821d14 100644 --- a/src/lerobot/scripts/lerobot_setup_motors.py +++ b/src/lerobot/scripts/lerobot_setup_motors.py @@ -34,16 +34,14 @@ lekiwi, make_robot_from_config, omx_follower, - so100_follower, - so101_follower, + so_follower, ) from lerobot.teleoperators import ( # noqa: F401 TeleoperatorConfig, koch_leader, make_teleoperator_from_config, omx_leader, - so100_leader, - so101_leader, + so_leader, ) COMPATIBLE_DEVICES = [ diff --git a/src/lerobot/scripts/lerobot_teleoperate.py b/src/lerobot/scripts/lerobot_teleoperate.py index 9866e76291..860285fca9 100644 --- a/src/lerobot/scripts/lerobot_teleoperate.py +++ b/src/lerobot/scripts/lerobot_teleoperate.py @@ -76,8 +76,7 @@ koch_follower, make_robot_from_config, omx_follower, - so100_follower, - so101_follower, + so_follower, ) from lerobot.teleoperators import ( # noqa: F401 Teleoperator, @@ -89,8 +88,7 @@ koch_leader, make_teleoperator_from_config, omx_leader, - so100_leader, - so101_leader, + so_leader, ) from lerobot.utils.import_utils import register_third_party_plugins from lerobot.utils.robot_utils import precise_sleep diff --git a/src/lerobot/teleoperators/bi_so100_leader/bi_so100_leader.py b/src/lerobot/teleoperators/bi_so100_leader/bi_so100_leader.py index 7696696557..93f66eb2e9 100644 --- a/src/lerobot/teleoperators/bi_so100_leader/bi_so100_leader.py +++ b/src/lerobot/teleoperators/bi_so100_leader/bi_so100_leader.py @@ -17,8 +17,7 @@ import logging from functools import cached_property -from lerobot.teleoperators.so100_leader.config_so100_leader import SO100LeaderConfig -from lerobot.teleoperators.so100_leader.so100_leader import SO100Leader +from lerobot.teleoperators.so_leader import SO100Leader, SO100LeaderConfig from ..teleoperator import Teleoperator from .config_bi_so100_leader import BiSO100LeaderConfig diff --git a/src/lerobot/teleoperators/so100_leader/so100_leader.py b/src/lerobot/teleoperators/so100_leader/so100_leader.py deleted file mode 100644 index edcfe53e6b..0000000000 --- a/src/lerobot/teleoperators/so100_leader/so100_leader.py +++ /dev/null @@ -1,159 +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. - -import logging -import time - -from lerobot.motors import Motor, MotorCalibration, MotorNormMode -from lerobot.motors.feetech import ( - FeetechMotorsBus, - OperatingMode, -) -from lerobot.utils.errors import DeviceAlreadyConnectedError, DeviceNotConnectedError - -from ..teleoperator import Teleoperator -from .config_so100_leader import SO100LeaderConfig - -logger = logging.getLogger(__name__) - - -class SO100Leader(Teleoperator): - """ - [SO-100 Leader Arm](https://github.com/TheRobotStudio/SO-ARM100) designed by TheRobotStudio - """ - - config_class = SO100LeaderConfig - name = "so100_leader" - - def __init__(self, config: SO100LeaderConfig): - super().__init__(config) - self.config = config - self.bus = FeetechMotorsBus( - port=self.config.port, - motors={ - "shoulder_pan": Motor(1, "sts3215", MotorNormMode.RANGE_M100_100), - "shoulder_lift": Motor(2, "sts3215", MotorNormMode.RANGE_M100_100), - "elbow_flex": Motor(3, "sts3215", MotorNormMode.RANGE_M100_100), - "wrist_flex": Motor(4, "sts3215", MotorNormMode.RANGE_M100_100), - "wrist_roll": Motor(5, "sts3215", MotorNormMode.RANGE_M100_100), - "gripper": Motor(6, "sts3215", MotorNormMode.RANGE_0_100), - }, - calibration=self.calibration, - ) - - @property - def action_features(self) -> dict[str, type]: - return {f"{motor}.pos": float for motor in self.bus.motors} - - @property - def feedback_features(self) -> dict[str, type]: - return {} - - @property - def is_connected(self) -> bool: - return self.bus.is_connected - - def connect(self, calibrate: bool = True) -> None: - if self.is_connected: - raise DeviceAlreadyConnectedError(f"{self} already connected") - - self.bus.connect() - if not self.is_calibrated and calibrate: - logger.info( - "Mismatch between calibration values in the motor and the calibration file or no calibration file found" - ) - self.calibrate() - - self.configure() - logger.info(f"{self} connected.") - - @property - def is_calibrated(self) -> bool: - return self.bus.is_calibrated - - def calibrate(self) -> None: - if self.calibration: - # Calibration file exists, ask user whether to use it or run new calibration - user_input = input( - f"Press ENTER to use provided calibration file associated with the id {self.id}, or type 'c' and press ENTER to run calibration: " - ) - if user_input.strip().lower() != "c": - logger.info(f"Writing calibration file associated with the id {self.id} to the motors") - self.bus.write_calibration(self.calibration) - return - - logger.info(f"\nRunning calibration of {self}") - self.bus.disable_torque() - for motor in self.bus.motors: - self.bus.write("Operating_Mode", motor, OperatingMode.POSITION.value) - - input(f"Move {self} to the middle of its range of motion and press ENTER....") - homing_offsets = self.bus.set_half_turn_homings() - - full_turn_motor = "wrist_roll" - unknown_range_motors = [motor for motor in self.bus.motors if motor != full_turn_motor] - print( - f"Move all joints except '{full_turn_motor}' sequentially through their " - "entire ranges of motion.\nRecording positions. Press ENTER to stop..." - ) - range_mins, range_maxes = self.bus.record_ranges_of_motion(unknown_range_motors) - range_mins[full_turn_motor] = 0 - range_maxes[full_turn_motor] = 4095 - - self.calibration = {} - for motor, m in self.bus.motors.items(): - self.calibration[motor] = MotorCalibration( - id=m.id, - drive_mode=0, - homing_offset=homing_offsets[motor], - range_min=range_mins[motor], - range_max=range_maxes[motor], - ) - - self.bus.write_calibration(self.calibration) - self._save_calibration() - print(f"Calibration saved to {self.calibration_fpath}") - - def configure(self) -> None: - self.bus.disable_torque() - self.bus.configure_motors() - for motor in self.bus.motors: - self.bus.write("Operating_Mode", motor, OperatingMode.POSITION.value) - - def setup_motors(self) -> None: - for motor in reversed(self.bus.motors): - input(f"Connect the controller board to the '{motor}' motor only and press enter.") - self.bus.setup_motor(motor) - print(f"'{motor}' motor id set to {self.bus.motors[motor].id}") - - def get_action(self) -> dict[str, float]: - start = time.perf_counter() - action = self.bus.sync_read("Present_Position") - action = {f"{motor}.pos": val for motor, val in action.items()} - dt_ms = (time.perf_counter() - start) * 1e3 - logger.debug(f"{self} read action: {dt_ms:.1f}ms") - return action - - def send_feedback(self, feedback: dict[str, float]) -> None: - # TODO(rcadene, aliberts): Implement force feedback - raise NotImplementedError - - def disconnect(self) -> None: - if not self.is_connected: - DeviceNotConnectedError(f"{self} is not connected.") - - self.bus.disconnect() - logger.info(f"{self} disconnected.") diff --git a/src/lerobot/teleoperators/so_leader/__init__.py b/src/lerobot/teleoperators/so_leader/__init__.py new file mode 100644 index 0000000000..a1017f3b9a --- /dev/null +++ b/src/lerobot/teleoperators/so_leader/__init__.py @@ -0,0 +1,22 @@ +#!/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 .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/so100_leader/config_so100_leader.py b/src/lerobot/teleoperators/so_leader/so100_leader/config_so100_leader.py similarity index 83% rename from src/lerobot/teleoperators/so100_leader/config_so100_leader.py rename to src/lerobot/teleoperators/so_leader/so100_leader/config_so100_leader.py index a97949b7e7..092eb0cfc0 100644 --- a/src/lerobot/teleoperators/so100_leader/config_so100_leader.py +++ b/src/lerobot/teleoperators/so_leader/so100_leader/config_so100_leader.py @@ -16,11 +16,11 @@ from dataclasses import dataclass -from ..config import TeleoperatorConfig +from ...config import TeleoperatorConfig +from ..so_leader_config_base import SOLeaderConfigBase @TeleoperatorConfig.register_subclass("so100_leader") @dataclass -class SO100LeaderConfig(TeleoperatorConfig): - # Port to connect to the arm - port: str +class SO100LeaderConfig(SOLeaderConfigBase): + pass diff --git a/src/lerobot/teleoperators/so_leader/so100_leader/so100.md b/src/lerobot/teleoperators/so_leader/so100_leader/so100.md new file mode 120000 index 0000000000..f06f88ff63 --- /dev/null +++ b/src/lerobot/teleoperators/so_leader/so100_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/so100_leader.py b/src/lerobot/teleoperators/so_leader/so100_leader/so100_leader.py new file mode 100644 index 0000000000..530e4f7234 --- /dev/null +++ b/src/lerobot/teleoperators/so_leader/so100_leader/so100_leader.py @@ -0,0 +1,27 @@ +# !/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/so101_leader/config_so101_leader.py b/src/lerobot/teleoperators/so_leader/so101_leader/config_so101_leader.py similarity index 81% rename from src/lerobot/teleoperators/so101_leader/config_so101_leader.py rename to src/lerobot/teleoperators/so_leader/so101_leader/config_so101_leader.py index 8d91c32dfe..1a6bb0df47 100644 --- a/src/lerobot/teleoperators/so101_leader/config_so101_leader.py +++ b/src/lerobot/teleoperators/so_leader/so101_leader/config_so101_leader.py @@ -16,13 +16,11 @@ from dataclasses import dataclass -from ..config import TeleoperatorConfig +from ...config import TeleoperatorConfig +from ..so_leader_config_base import SOLeaderConfigBase @TeleoperatorConfig.register_subclass("so101_leader") @dataclass -class SO101LeaderConfig(TeleoperatorConfig): - # Port to connect to the arm - port: str - - use_degrees: bool = False +class SO101LeaderConfig(SOLeaderConfigBase): + pass diff --git a/src/lerobot/teleoperators/so_leader/so101_leader/so101.md b/src/lerobot/teleoperators/so_leader/so101_leader/so101.md new file mode 120000 index 0000000000..38f4deca72 --- /dev/null +++ b/src/lerobot/teleoperators/so_leader/so101_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/so101_leader.py b/src/lerobot/teleoperators/so_leader/so101_leader/so101_leader.py new file mode 100644 index 0000000000..3aed1f6f9c --- /dev/null +++ b/src/lerobot/teleoperators/so_leader/so101_leader/so101_leader.py @@ -0,0 +1,27 @@ +# !/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/so101_leader/so101_leader.py b/src/lerobot/teleoperators/so_leader/so_leader_base.py similarity index 87% rename from src/lerobot/teleoperators/so101_leader/so101_leader.py rename to src/lerobot/teleoperators/so_leader/so_leader_base.py index be804bf702..6cdaca2e76 100644 --- a/src/lerobot/teleoperators/so101_leader/so101_leader.py +++ b/src/lerobot/teleoperators/so_leader/so_leader_base.py @@ -1,6 +1,6 @@ -#!/usr/bin/env python +# !/usr/bin/env python -# Copyright 2025 The HuggingFace Inc. team. All rights reserved. +# 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. @@ -25,20 +25,15 @@ from lerobot.utils.errors import DeviceAlreadyConnectedError, DeviceNotConnectedError from ..teleoperator import Teleoperator -from .config_so101_leader import SO101LeaderConfig +from .so_leader_config_base import SOLeaderConfigBase logger = logging.getLogger(__name__) -class SO101Leader(Teleoperator): - """ - SO-101 Leader Arm designed by TheRobotStudio and Hugging Face. - """ +class SOLeaderBase(Teleoperator): + """Generic SO leader base for SO-100/101/10X teleoperators.""" - config_class = SO101LeaderConfig - name = "so101_leader" - - def __init__(self, config: SO101LeaderConfig): + def __init__(self, config: SOLeaderConfigBase): super().__init__(config) self.config = config norm_mode_body = MotorNormMode.DEGREES if config.use_degrees else MotorNormMode.RANGE_M100_100 @@ -104,11 +99,15 @@ def calibrate(self) -> None: input(f"Move {self} to the middle of its range of motion and press ENTER....") homing_offsets = self.bus.set_half_turn_homings() + full_turn_motor = "wrist_roll" + unknown_range_motors = [motor for motor in self.bus.motors if motor != full_turn_motor] print( - "Move all joints sequentially through their entire ranges " - "of motion.\nRecording positions. Press ENTER to stop..." + f"Move all joints except '{full_turn_motor}' sequentially through their " + "entire ranges of motion.\nRecording positions. Press ENTER to stop..." ) - range_mins, range_maxes = self.bus.record_ranges_of_motion() + range_mins, range_maxes = self.bus.record_ranges_of_motion(unknown_range_motors) + range_mins[full_turn_motor] = 0 + range_maxes[full_turn_motor] = 4095 self.calibration = {} for motor, m in self.bus.motors.items(): @@ -145,7 +144,7 @@ def get_action(self) -> dict[str, float]: return action def send_feedback(self, feedback: dict[str, float]) -> None: - # TODO(rcadene, aliberts): Implement force feedback + # TODO: Implement force feedback raise NotImplementedError def disconnect(self) -> None: diff --git a/src/lerobot/robots/so100_follower/__init__.py b/src/lerobot/teleoperators/so_leader/so_leader_config_base.py similarity index 66% rename from src/lerobot/robots/so100_follower/__init__.py rename to src/lerobot/teleoperators/so_leader/so_leader_config_base.py index 5dc43ac3bc..b2f2dfcfa7 100644 --- a/src/lerobot/robots/so100_follower/__init__.py +++ b/src/lerobot/teleoperators/so_leader/so_leader_config_base.py @@ -14,5 +14,17 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .config_so100_follower import SO100FollowerConfig -from .so100_follower import SO100Follower +from dataclasses import dataclass + +from ..config import TeleoperatorConfig + + +@dataclass +class SOLeaderConfigBase(TeleoperatorConfig): + """Base configuration class for SO Leader teleoperators.""" + + # Port to connect to the arm + port: str + + # Whether to use degrees for angles + use_degrees: bool = False diff --git a/src/lerobot/teleoperators/utils.py b/src/lerobot/teleoperators/utils.py index 699d1253f6..74e43ec950 100644 --- a/src/lerobot/teleoperators/utils.py +++ b/src/lerobot/teleoperators/utils.py @@ -46,11 +46,11 @@ def make_teleoperator_from_config(config: TeleoperatorConfig) -> Teleoperator: return OmxLeader(config) elif config.type == "so100_leader": - from .so100_leader import SO100Leader + from .so_leader import SO100Leader return SO100Leader(config) elif config.type == "so101_leader": - from .so101_leader import SO101Leader + from .so_leader import SO101Leader return SO101Leader(config) elif config.type == "mock_teleop": diff --git a/tests/robots/test_so100_follower.py b/tests/robots/test_so100_follower.py index d76b9591a8..fc300b8202 100644 --- a/tests/robots/test_so100_follower.py +++ b/tests/robots/test_so100_follower.py @@ -19,7 +19,7 @@ import pytest -from lerobot.robots.so100_follower import ( +from lerobot.robots.so_follower import ( SO100Follower, SO100FollowerConfig, ) @@ -66,7 +66,7 @@ def _bus_side_effect(*_args, **kwargs): with ( patch( - "lerobot.robots.so100_follower.so100_follower.FeetechMotorsBus", + "lerobot.robots.so_follower.so_follower_base.FeetechMotorsBus", side_effect=_bus_side_effect, ), patch.object(SO100Follower, "configure", lambda self: None),