Merge remote-tracking branch 'origin/user/aliberts/2025_02_25_refactor_robots' into user/aliberts/2025_04_03_add_hope_jr

This commit is contained in:
Simon Alibert
2025-05-22 11:35:09 +02:00
17 changed files with 86 additions and 333 deletions

View File

@@ -1,68 +0,0 @@
{
"homing_offset": [
2048,
3072,
3072,
-1024,
-1024,
2048,
-2048,
2048,
-2048
],
"drive_mode": [
1,
1,
1,
0,
0,
1,
0,
1,
0
],
"start_pos": [
2015,
3058,
3061,
1071,
1071,
2035,
2152,
2029,
2499
],
"end_pos": [
-1008,
-1963,
-1966,
2141,
2143,
-971,
3043,
-1077,
3144
],
"calib_mode": [
"DEGREE",
"DEGREE",
"DEGREE",
"DEGREE",
"DEGREE",
"DEGREE",
"DEGREE",
"DEGREE",
"LINEAR"
],
"motor_names": [
"waist",
"shoulder",
"shoulder_shadow",
"elbow",
"elbow_shadow",
"forearm_roll",
"wrist_angle",
"wrist_rotate",
"gripper"
]
}

View File

@@ -1,68 +0,0 @@
{
"homing_offset": [
2048,
3072,
3072,
-1024,
-1024,
2048,
-2048,
2048,
-1024
],
"drive_mode": [
1,
1,
1,
0,
0,
1,
0,
1,
0
],
"start_pos": [
2035,
3024,
3019,
979,
981,
1982,
2166,
2124,
1968
],
"end_pos": [
-990,
-2017,
-2015,
2078,
2076,
-1030,
3117,
-1016,
2556
],
"calib_mode": [
"DEGREE",
"DEGREE",
"DEGREE",
"DEGREE",
"DEGREE",
"DEGREE",
"DEGREE",
"DEGREE",
"LINEAR"
],
"motor_names": [
"waist",
"shoulder",
"shoulder_shadow",
"elbow",
"elbow_shadow",
"forearm_roll",
"wrist_angle",
"wrist_rotate",
"gripper"
]
}

View File

@@ -1,68 +0,0 @@
{
"homing_offset": [
2048,
3072,
3072,
-1024,
-1024,
2048,
-2048,
2048,
-2048
],
"drive_mode": [
1,
1,
1,
0,
0,
1,
0,
1,
0
],
"start_pos": [
2056,
2895,
2896,
1191,
1190,
2018,
2051,
2056,
2509
],
"end_pos": [
-1040,
-2004,
-2006,
2126,
2127,
-1010,
3050,
-1117,
3143
],
"calib_mode": [
"DEGREE",
"DEGREE",
"DEGREE",
"DEGREE",
"DEGREE",
"DEGREE",
"DEGREE",
"DEGREE",
"LINEAR"
],
"motor_names": [
"waist",
"shoulder",
"shoulder_shadow",
"elbow",
"elbow_shadow",
"forearm_roll",
"wrist_angle",
"wrist_rotate",
"gripper"
]
}

View File

@@ -1,68 +0,0 @@
{
"homing_offset": [
2048,
3072,
3072,
-1024,
-1024,
2048,
-2048,
2048,
-2048
],
"drive_mode": [
1,
1,
1,
0,
0,
1,
0,
1,
0
],
"start_pos": [
2068,
3034,
3030,
1038,
1041,
1991,
1948,
2090,
1985
],
"end_pos": [
-1025,
-2014,
-2015,
2058,
2060,
-955,
3091,
-940,
2576
],
"calib_mode": [
"DEGREE",
"DEGREE",
"DEGREE",
"DEGREE",
"DEGREE",
"DEGREE",
"DEGREE",
"DEGREE",
"LINEAR"
],
"motor_names": [
"waist",
"shoulder",
"shoulder_shadow",
"elbow",
"elbow_shadow",
"forearm_roll",
"wrist_angle",
"wrist_rotate",
"gripper"
]
}

7
.gitignore vendored
View File

@@ -11,7 +11,10 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and # See the License for the specific language governing permissions and
# limitations under the License. # limitations under the License.
# Dev scripts
.dev .dev
# Logging # Logging
logs logs
tmp tmp
@@ -91,10 +94,8 @@ coverage.xml
.hypothesis/ .hypothesis/
.pytest_cache/ .pytest_cache/
# Ignore .cache except calibration # Ignore .cache
.cache/* .cache/*
!.cache/calibration/
!.cache/calibration/**
# Translations # Translations
*.mo *.mo

View File

@@ -55,7 +55,7 @@ conda install ffmpeg -c conda-forge
Install 🤗 LeRobot: Install 🤗 LeRobot:
```bash ```bash
cd lerobot && pip install ".[feetech]" cd lerobot && pip install -e ".[feetech]"
``` ```
## Troubleshooting ## Troubleshooting

View File

@@ -142,7 +142,6 @@ from lerobot.common.datasets.video_utils import (
get_video_info, get_video_info,
) )
from lerobot.common.robots import RobotConfig from lerobot.common.robots import RobotConfig
from lerobot.common.robots.utils import make_robot_config
V16 = "v1.6" V16 = "v1.6"
V20 = "v2.0" V20 = "v2.0"
@@ -598,6 +597,30 @@ def convert_dataset(
create_branch(repo_id=repo_id, branch=V20, repo_type="dataset") create_branch(repo_id=repo_id, branch=V20, repo_type="dataset")
def make_robot_config(robot_type: str, **kwargs) -> RobotConfig:
if robot_type == "aloha":
raise NotImplementedError # TODO
elif robot_type == "koch_follower":
from lerobot.common.robots.koch_follower import KochFollowerConfig
return KochFollowerConfig(**kwargs)
elif robot_type == "so100_follower":
from lerobot.common.robots.so100_follower import SO100FollowerConfig
return SO100FollowerConfig(**kwargs)
elif robot_type == "stretch":
from lerobot.common.robots.stretch3 import Stretch3RobotConfig
return Stretch3RobotConfig(**kwargs)
elif robot_type == "lekiwi":
from lerobot.common.robots.lekiwi import LeKiwiConfig
return LeKiwiConfig(**kwargs)
else:
raise ValueError(f"Robot type '{robot_type}' is not available.")
def main(): def main():
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
task_args = parser.add_mutually_exclusive_group(required=True) task_args = parser.add_mutually_exclusive_group(required=True)

View File

@@ -32,7 +32,8 @@ class EnvConfig(draccus.ChoiceRegistry, abc.ABC):
def type(self) -> str: def type(self) -> str:
return self.get_choice_name(self.__class__) return self.get_choice_name(self.__class__)
@abc.abstractproperty @property
@abc.abstractmethod
def gym_kwargs(self) -> dict: def gym_kwargs(self) -> dict:
raise NotImplementedError() raise NotImplementedError()

View File

@@ -108,6 +108,7 @@ class DynamixelMotorsBus(MotorsBus):
https://emanual.robotis.com/docs/en/software/dynamixel/dynamixel_sdk/sample_code/python_read_write_protocol_2_0/#python-read-write-protocol-20 https://emanual.robotis.com/docs/en/software/dynamixel/dynamixel_sdk/sample_code/python_read_write_protocol_2_0/#python-read-write-protocol-20
""" """
apply_drive_mode = False
available_baudrates = deepcopy(AVAILABLE_BAUDRATES) available_baudrates = deepcopy(AVAILABLE_BAUDRATES)
default_baudrate = DEFAULT_BAUDRATE default_baudrate = DEFAULT_BAUDRATE
default_timeout = DEFAULT_TIMEOUT_MS default_timeout = DEFAULT_TIMEOUT_MS

View File

@@ -102,6 +102,7 @@ class FeetechMotorsBus(MotorsBus):
python feetech sdk to communicate with the motors, which is itself based on the dynamixel sdk. python feetech sdk to communicate with the motors, which is itself based on the dynamixel sdk.
""" """
apply_drive_mode = True
available_baudrates = deepcopy(SCAN_BAUDRATES) available_baudrates = deepcopy(SCAN_BAUDRATES)
default_baudrate = DEFAULT_BAUDRATE default_baudrate = DEFAULT_BAUDRATE
default_timeout = DEFAULT_TIMEOUT_MS default_timeout = DEFAULT_TIMEOUT_MS

View File

@@ -252,6 +252,7 @@ class MotorsBus(abc.ABC):
``` ```
""" """
apply_drive_mode: bool
available_baudrates: list[int] available_baudrates: list[int]
default_baudrate: int default_baudrate: int
default_timeout: int default_timeout: int
@@ -756,7 +757,7 @@ class MotorsBus(abc.ABC):
return mins, maxes return mins, maxes
def _normalize(self, data_name: str, ids_values: dict[int, int]) -> dict[int, float]: def _normalize(self, ids_values: dict[int, int]) -> dict[int, float]:
if not self.calibration: if not self.calibration:
raise RuntimeError(f"{self} has no calibration registered.") raise RuntimeError(f"{self} has no calibration registered.")
@@ -765,20 +766,24 @@ class MotorsBus(abc.ABC):
motor = self._id_to_name(id_) motor = self._id_to_name(id_)
min_ = self.calibration[motor].range_min min_ = self.calibration[motor].range_min
max_ = self.calibration[motor].range_max max_ = self.calibration[motor].range_max
drive_mode = self.apply_drive_mode and self.calibration[motor].drive_mode
if max_ == min_:
raise ValueError(f"Invalid calibration for motor '{motor}': min and max are equal.")
bounded_val = min(max_, max(min_, val)) bounded_val = min(max_, max(min_, val))
# TODO(Steven): normalization can go boom if max_ == min_, we should add a check probably in record_ranges_of_motions
# (which probably indicates the user forgot to move a motor, most likely a gripper-like one)
if self.motors[motor].norm_mode is MotorNormMode.RANGE_M100_100: if self.motors[motor].norm_mode is MotorNormMode.RANGE_M100_100:
normalized_values[id_] = (((bounded_val - min_) / (max_ - min_)) * 200) - 100 norm = (((bounded_val - min_) / (max_ - min_)) * 200) - 100
normalized_values[id_] = -norm if drive_mode else norm
elif self.motors[motor].norm_mode is MotorNormMode.RANGE_0_100: elif self.motors[motor].norm_mode is MotorNormMode.RANGE_0_100:
normalized_values[id_] = ((bounded_val - min_) / (max_ - min_)) * 100 norm = ((bounded_val - min_) / (max_ - min_)) * 100
normalized_values[id_] = 100 - norm if drive_mode else norm
else: else:
# TODO(alibers): velocity and degree modes # TODO(alibers): degree mode
raise NotImplementedError raise NotImplementedError
return normalized_values return normalized_values
def _unnormalize(self, data_name: str, ids_values: dict[int, float]) -> dict[int, int]: def _unnormalize(self, ids_values: dict[int, float]) -> dict[int, int]:
if not self.calibration: if not self.calibration:
raise RuntimeError(f"{self} has no calibration registered.") raise RuntimeError(f"{self} has no calibration registered.")
@@ -787,14 +792,20 @@ class MotorsBus(abc.ABC):
motor = self._id_to_name(id_) motor = self._id_to_name(id_)
min_ = self.calibration[motor].range_min min_ = self.calibration[motor].range_min
max_ = self.calibration[motor].range_max max_ = self.calibration[motor].range_max
drive_mode = self.apply_drive_mode and self.calibration[motor].drive_mode
if max_ == min_:
raise ValueError(f"Invalid calibration for motor '{motor}': min and max are equal.")
if self.motors[motor].norm_mode is MotorNormMode.RANGE_M100_100: if self.motors[motor].norm_mode is MotorNormMode.RANGE_M100_100:
val = -val if drive_mode else val
bounded_val = min(100.0, max(-100.0, val)) bounded_val = min(100.0, max(-100.0, val))
unnormalized_values[id_] = int(((bounded_val + 100) / 200) * (max_ - min_) + min_) unnormalized_values[id_] = int(((bounded_val + 100) / 200) * (max_ - min_) + min_)
elif self.motors[motor].norm_mode is MotorNormMode.RANGE_0_100: elif self.motors[motor].norm_mode is MotorNormMode.RANGE_0_100:
val = 100 - val if drive_mode else val
bounded_val = min(100.0, max(0.0, val)) bounded_val = min(100.0, max(0.0, val))
unnormalized_values[id_] = int((bounded_val / 100) * (max_ - min_) + min_) unnormalized_values[id_] = int((bounded_val / 100) * (max_ - min_) + min_)
else: else:
# TODO(alibers): velocity and degree modes # TODO(aliberts): degree mode
raise NotImplementedError raise NotImplementedError
return unnormalized_values return unnormalized_values
@@ -915,7 +926,7 @@ class MotorsBus(abc.ABC):
id_value = self._decode_sign(data_name, {id_: value}) id_value = self._decode_sign(data_name, {id_: value})
if normalize and data_name in self.normalized_data: if normalize and data_name in self.normalized_data:
id_value = self._normalize(data_name, id_value) id_value = self._normalize(id_value)
return id_value[id_] return id_value[id_]
@@ -982,7 +993,7 @@ class MotorsBus(abc.ABC):
addr, length = get_address(self.model_ctrl_table, model, data_name) addr, length = get_address(self.model_ctrl_table, model, data_name)
if normalize and data_name in self.normalized_data: if normalize and data_name in self.normalized_data:
value = self._unnormalize(data_name, {id_: value})[id_] value = self._unnormalize({id_: value})[id_]
value = self._encode_sign(data_name, {id_: value})[id_] value = self._encode_sign(data_name, {id_: value})[id_]
@@ -1061,7 +1072,7 @@ class MotorsBus(abc.ABC):
ids_values = self._decode_sign(data_name, ids_values) ids_values = self._decode_sign(data_name, ids_values)
if normalize and data_name in self.normalized_data: if normalize and data_name in self.normalized_data:
ids_values = self._normalize(data_name, ids_values) ids_values = self._normalize(ids_values)
return {self._id_to_name(id_): value for id_, value in ids_values.items()} return {self._id_to_name(id_): value for id_, value in ids_values.items()}
@@ -1147,7 +1158,7 @@ class MotorsBus(abc.ABC):
addr, length = get_address(self.model_ctrl_table, model, data_name) addr, length = get_address(self.model_ctrl_table, model, data_name)
if normalize and data_name in self.normalized_data: if normalize and data_name in self.normalized_data:
ids_values = self._unnormalize(data_name, ids_values) ids_values = self._unnormalize(ids_values)
ids_values = self._encode_sign(data_name, ids_values) ids_values = self._encode_sign(data_name, ids_values)

View File

@@ -49,15 +49,18 @@ class Robot(abc.ABC):
return f"{self.id} {self.__class__.__name__}" return f"{self.id} {self.__class__.__name__}"
# TODO(aliberts): create a proper Feature class for this that links with datasets # TODO(aliberts): create a proper Feature class for this that links with datasets
@abc.abstractproperty @property
@abc.abstractmethod
def observation_features(self) -> dict: def observation_features(self) -> dict:
pass pass
@abc.abstractproperty @property
@abc.abstractmethod
def action_features(self) -> dict: def action_features(self) -> dict:
pass pass
@abc.abstractproperty @property
@abc.abstractmethod
def is_connected(self) -> bool: def is_connected(self) -> bool:
pass pass
@@ -66,7 +69,8 @@ class Robot(abc.ABC):
"""Connects to the robot.""" """Connects to the robot."""
pass pass
@abc.abstractproperty @property
@abc.abstractmethod
def is_calibrated(self) -> bool: def is_calibrated(self) -> bool:
pass pass

View File

@@ -61,7 +61,7 @@ conda install ffmpeg -c conda-forge
Install 🤗 LeRobot: Install 🤗 LeRobot:
```bash ```bash
cd lerobot && pip install ".[feetech]" cd lerobot && pip install -e ".[feetech]"
``` ```
> [!NOTE] > [!NOTE]

View File

@@ -20,30 +20,6 @@ from lerobot.common.robots import RobotConfig
from .robot import Robot from .robot import Robot
def make_robot_config(robot_type: str, **kwargs) -> RobotConfig:
if robot_type == "aloha":
raise NotImplementedError # TODO
elif robot_type == "koch_follower":
from .koch_follower.config_koch_follower import KochFollowerConfig
return KochFollowerConfig(**kwargs)
elif robot_type == "so100_follower":
from .so100_follower.config_so100_follower import SO100FollowerConfig
return SO100FollowerConfig(**kwargs)
elif robot_type == "stretch":
from .stretch3.configuration_stretch3 import Stretch3RobotConfig
return Stretch3RobotConfig(**kwargs)
elif robot_type == "lekiwi":
from .lekiwi.config_lekiwi import LeKiwiConfig
return LeKiwiConfig(**kwargs)
else:
raise ValueError(f"Robot type '{robot_type}' is not available.")
def make_robot_from_config(config: RobotConfig) -> Robot: def make_robot_from_config(config: RobotConfig) -> Robot:
if config.type == "koch_follower": if config.type == "koch_follower":
from .koch_follower import KochFollower from .koch_follower import KochFollower

View File

@@ -47,15 +47,18 @@ class Teleoperator(abc.ABC):
def __str__(self) -> str: def __str__(self) -> str:
return f"{self.id} {self.__class__.__name__}" return f"{self.id} {self.__class__.__name__}"
@abc.abstractproperty @property
@abc.abstractmethod
def action_features(self) -> dict: def action_features(self) -> dict:
pass pass
@abc.abstractproperty @property
@abc.abstractmethod
def feedback_features(self) -> dict: def feedback_features(self) -> dict:
pass pass
@abc.abstractproperty @property
@abc.abstractmethod
def is_connected(self) -> bool: def is_connected(self) -> bool:
pass pass
@@ -64,7 +67,8 @@ class Teleoperator(abc.ABC):
"""Connects to the teleoperator.""" """Connects to the teleoperator."""
pass pass
@abc.abstractproperty @property
@abc.abstractmethod
def is_calibrated(self) -> bool: def is_calibrated(self) -> bool:
pass pass

View File

@@ -78,15 +78,18 @@ class PreTrainedConfig(draccus.ChoiceRegistry, HubMixin, abc.ABC):
def type(self) -> str: def type(self) -> str:
return self.get_choice_name(self.__class__) return self.get_choice_name(self.__class__)
@abc.abstractproperty @property
@abc.abstractmethod
def observation_delta_indices(self) -> list | None: def observation_delta_indices(self) -> list | None:
raise NotImplementedError raise NotImplementedError
@abc.abstractproperty @property
@abc.abstractmethod
def action_delta_indices(self) -> list | None: def action_delta_indices(self) -> list | None:
raise NotImplementedError raise NotImplementedError
@abc.abstractproperty @property
@abc.abstractmethod
def reward_delta_indices(self) -> list | None: def reward_delta_indices(self) -> list | None:
raise NotImplementedError raise NotImplementedError

View File

@@ -125,7 +125,7 @@ def test_read(data_name, id_, value, dummy_motors):
) )
mock__decode_sign.assert_called_once_with(data_name, {id_: value}) mock__decode_sign.assert_called_once_with(data_name, {id_: value})
if data_name in bus.normalized_data: if data_name in bus.normalized_data:
mock__normalize.assert_called_once_with(data_name, {id_: value}) mock__normalize.assert_called_once_with({id_: value})
@pytest.mark.parametrize( @pytest.mark.parametrize(
@@ -159,7 +159,7 @@ def test_write(data_name, id_, value, dummy_motors):
) )
mock__encode_sign.assert_called_once_with(data_name, {id_: value}) mock__encode_sign.assert_called_once_with(data_name, {id_: value})
if data_name in bus.normalized_data: if data_name in bus.normalized_data:
mock__unnormalize.assert_called_once_with(data_name, {id_: value}) mock__unnormalize.assert_called_once_with({id_: value})
@pytest.mark.parametrize( @pytest.mark.parametrize(
@@ -196,7 +196,7 @@ def test_sync_read_by_str(data_name, id_, value, dummy_motors):
) )
mock__decode_sign.assert_called_once_with(data_name, {id_: value}) mock__decode_sign.assert_called_once_with(data_name, {id_: value})
if data_name in bus.normalized_data: if data_name in bus.normalized_data:
mock__normalize.assert_called_once_with(data_name, {id_: value}) mock__normalize.assert_called_once_with({id_: value})
@pytest.mark.parametrize( @pytest.mark.parametrize(
@@ -233,7 +233,7 @@ def test_sync_read_by_list(data_name, ids_values, dummy_motors):
) )
mock__decode_sign.assert_called_once_with(data_name, ids_values) mock__decode_sign.assert_called_once_with(data_name, ids_values)
if data_name in bus.normalized_data: if data_name in bus.normalized_data:
mock__normalize.assert_called_once_with(data_name, ids_values) mock__normalize.assert_called_once_with(ids_values)
@pytest.mark.parametrize( @pytest.mark.parametrize(
@@ -270,7 +270,7 @@ def test_sync_read_by_none(data_name, ids_values, dummy_motors):
) )
mock__decode_sign.assert_called_once_with(data_name, ids_values) mock__decode_sign.assert_called_once_with(data_name, ids_values)
if data_name in bus.normalized_data: if data_name in bus.normalized_data:
mock__normalize.assert_called_once_with(data_name, ids_values) mock__normalize.assert_called_once_with(ids_values)
@pytest.mark.parametrize( @pytest.mark.parametrize(
@@ -304,7 +304,7 @@ def test_sync_write_by_single_value(data_name, value, dummy_motors):
) )
mock__encode_sign.assert_called_once_with(data_name, ids_values) mock__encode_sign.assert_called_once_with(data_name, ids_values)
if data_name in bus.normalized_data: if data_name in bus.normalized_data:
mock__unnormalize.assert_called_once_with(data_name, ids_values) mock__unnormalize.assert_called_once_with(ids_values)
@pytest.mark.parametrize( @pytest.mark.parametrize(
@@ -339,4 +339,4 @@ def test_sync_write_by_value_dict(data_name, ids_values, dummy_motors):
) )
mock__encode_sign.assert_called_once_with(data_name, ids_values) mock__encode_sign.assert_called_once_with(data_name, ids_values)
if data_name in bus.normalized_data: if data_name in bus.normalized_data:
mock__unnormalize.assert_called_once_with(data_name, ids_values) mock__unnormalize.assert_called_once_with(ids_values)