refactor: keep GELLO hardware patches in main repository
This commit is contained in:
parent
2c59ff82ba
commit
652562a7b7
6
.gitignore
vendored
6
.gitignore
vendored
@ -89,4 +89,8 @@ models/
|
|||||||
*.xvcd
|
*.xvcd
|
||||||
ufactory_usage/
|
ufactory_usage/
|
||||||
.history/
|
.history/
|
||||||
datasets/
|
datasets/
|
||||||
|
|
||||||
|
# Local checkout used for GELLO hardware development. Runtime fixes live in
|
||||||
|
# the main package so the repository does not depend on a modified submodule.
|
||||||
|
third_party/gello_software/
|
||||||
|
|||||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@ -1,3 +0,0 @@
|
|||||||
[submodule "third_party/gello_software"]
|
|
||||||
path = third_party/gello_software
|
|
||||||
url = https://github.com/xArm-Developer/gello_software.git
|
|
||||||
@ -45,7 +45,7 @@ uf-camera-test = "lerobot_robot_ufactory.scripts.uf_camera_test:main"
|
|||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
# GELLO 遥操作
|
# GELLO 遥操作
|
||||||
gello = [
|
gello = [
|
||||||
"gello",
|
"gello @ git+https://github.com/xArm-Developer/gello_software.git",
|
||||||
"dynamixel-sdk>=4.0.5",
|
"dynamixel-sdk>=4.0.5",
|
||||||
]
|
]
|
||||||
# SpaceMouse 遥操作
|
# SpaceMouse 遥操作
|
||||||
@ -70,9 +70,6 @@ where = ["src"]
|
|||||||
[tool.setuptools.package-data]
|
[tool.setuptools.package-data]
|
||||||
"lerobot_robot_ufactory.devices.umi.xvlib" = ["*.so", "*.so.*"]
|
"lerobot_robot_ufactory.devices.umi.xvlib" = ["*.so", "*.so.*"]
|
||||||
|
|
||||||
[tool.uv.sources]
|
|
||||||
gello = { path = "third_party/gello_software", editable = true }
|
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
line-length = 100
|
line-length = 100
|
||||||
target-version = "py310"
|
target-version = "py310"
|
||||||
|
|||||||
@ -0,0 +1,154 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional, Sequence, Tuple
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from dynamixel_sdk import COMM_SUCCESS
|
||||||
|
from dynamixel_sdk.robotis_def import (
|
||||||
|
DXL_HIBYTE,
|
||||||
|
DXL_HIWORD,
|
||||||
|
DXL_LOBYTE,
|
||||||
|
DXL_LOWORD,
|
||||||
|
)
|
||||||
|
from gello.dynamixel import driver as driver_module
|
||||||
|
from gello.dynamixel.driver import DynamixelDriver
|
||||||
|
from gello.robots.dynamixel import DynamixelRobot
|
||||||
|
|
||||||
|
|
||||||
|
class SafeDynamixelDriver(DynamixelDriver):
|
||||||
|
"""GELLO driver with serialized writes and complete torque cleanup."""
|
||||||
|
|
||||||
|
def set_joints(self, joint_angles: Sequence[float]) -> None:
|
||||||
|
if len(joint_angles) != len(self._ids):
|
||||||
|
raise ValueError("joint_angles must match the configured Dynamixel IDs")
|
||||||
|
if not self._torque_enabled:
|
||||||
|
raise RuntimeError("Torque must be enabled to set joint angles")
|
||||||
|
if self._is_fake:
|
||||||
|
self._fake_joint_angles = np.asarray(joint_angles, dtype=float)
|
||||||
|
return
|
||||||
|
|
||||||
|
with self._lock:
|
||||||
|
try:
|
||||||
|
for dxl_id, angle in zip(self._ids, joint_angles, strict=True):
|
||||||
|
position_value = int(angle * 2048 / np.pi)
|
||||||
|
parameter = [
|
||||||
|
DXL_LOBYTE(DXL_LOWORD(position_value)),
|
||||||
|
DXL_HIBYTE(DXL_LOWORD(position_value)),
|
||||||
|
DXL_LOBYTE(DXL_HIWORD(position_value)),
|
||||||
|
DXL_HIBYTE(DXL_HIWORD(position_value)),
|
||||||
|
]
|
||||||
|
if not self._groupSyncWrite.addParam(dxl_id, parameter):
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Failed to set joint angle for Dynamixel ID {dxl_id}"
|
||||||
|
)
|
||||||
|
|
||||||
|
result = self._groupSyncWrite.txPacket()
|
||||||
|
if result != COMM_SUCCESS:
|
||||||
|
detail = self._packetHandler.getTxRxResult(result)
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Failed to syncwrite goal position: {detail} ({result})"
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
self._groupSyncWrite.clearParam()
|
||||||
|
|
||||||
|
def set_torque_mode(self, enable: bool) -> None:
|
||||||
|
if self._is_fake:
|
||||||
|
self._torque_enabled = enable
|
||||||
|
return
|
||||||
|
|
||||||
|
torque_value = driver_module.TORQUE_ENABLE if enable else driver_module.TORQUE_DISABLE
|
||||||
|
failures = []
|
||||||
|
with self._lock:
|
||||||
|
for dxl_id in self._ids:
|
||||||
|
result, error = self._packetHandler.write1ByteTxRx(
|
||||||
|
self._portHandler,
|
||||||
|
dxl_id,
|
||||||
|
driver_module.ADDR_TORQUE_ENABLE,
|
||||||
|
torque_value,
|
||||||
|
)
|
||||||
|
if result != COMM_SUCCESS:
|
||||||
|
detail = self._packetHandler.getTxRxResult(result)
|
||||||
|
failures.append(f"ID {dxl_id}: {detail} ({result})")
|
||||||
|
continue
|
||||||
|
if error == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if not enable:
|
||||||
|
state, read_result, _ = self._packetHandler.read1ByteTxRx(
|
||||||
|
self._portHandler,
|
||||||
|
dxl_id,
|
||||||
|
driver_module.ADDR_TORQUE_ENABLE,
|
||||||
|
)
|
||||||
|
if read_result == COMM_SUCCESS and state == driver_module.TORQUE_DISABLE:
|
||||||
|
continue
|
||||||
|
|
||||||
|
detail = self._packetHandler.getRxPacketError(error)
|
||||||
|
failures.append(f"ID {dxl_id}: {detail} ({error})")
|
||||||
|
|
||||||
|
if failures:
|
||||||
|
raise RuntimeError("Failed to set torque mode: " + "; ".join(failures))
|
||||||
|
self._torque_enabled = enable
|
||||||
|
|
||||||
|
|
||||||
|
class ContinuousDynamixelRobot(DynamixelRobot):
|
||||||
|
"""Dynamixel GELLO whose arm joints remain continuous across encoder wrap."""
|
||||||
|
|
||||||
|
def get_joint_state(self) -> np.ndarray:
|
||||||
|
pos = (self._driver.get_joints() - self._joint_offsets) * self._joint_signs
|
||||||
|
if len(pos) != self.num_dofs():
|
||||||
|
raise RuntimeError("Unexpected Dynamixel joint count")
|
||||||
|
|
||||||
|
arm_dofs = len(pos) - 1 if self.gripper_open_close is not None else len(pos)
|
||||||
|
if self._last_pos is not None:
|
||||||
|
pos[:arm_dofs] += 2 * np.pi * np.round(
|
||||||
|
(self._last_pos[:arm_dofs] - pos[:arm_dofs]) / (2 * np.pi)
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.gripper_open_close is not None:
|
||||||
|
gripper_open, gripper_close = self.gripper_open_close
|
||||||
|
gripper_pos = (pos[-1] - gripper_open) / (gripper_close - gripper_open)
|
||||||
|
pos[-1] = min(max(0.0, gripper_pos), 1.0)
|
||||||
|
|
||||||
|
if self._last_pos is None:
|
||||||
|
self._last_pos = pos
|
||||||
|
else:
|
||||||
|
pos = self._last_pos * (1 - self._alpha) + pos * self._alpha
|
||||||
|
self._last_pos = pos
|
||||||
|
return pos
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class PatchedDynamixelRobotConfig:
|
||||||
|
joint_ids: Sequence[int]
|
||||||
|
joint_offsets: Sequence[float]
|
||||||
|
joint_signs: Sequence[int]
|
||||||
|
gripper_config: Optional[Tuple[int, float, float]]
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
if len(self.joint_ids) != len(self.joint_offsets):
|
||||||
|
raise ValueError("joint_ids and joint_offsets must have the same length")
|
||||||
|
if len(self.joint_ids) != len(self.joint_signs):
|
||||||
|
raise ValueError("joint_ids and joint_signs must have the same length")
|
||||||
|
|
||||||
|
def make_robot(
|
||||||
|
self,
|
||||||
|
port: str = "/dev/ttyUSB0",
|
||||||
|
start_joints: Optional[np.ndarray] = None,
|
||||||
|
) -> ContinuousDynamixelRobot:
|
||||||
|
# Upstream DynamixelRobot imports its driver inside __init__. Replace
|
||||||
|
# that symbol only while constructing this instance.
|
||||||
|
original_driver = driver_module.DynamixelDriver
|
||||||
|
driver_module.DynamixelDriver = SafeDynamixelDriver
|
||||||
|
try:
|
||||||
|
return ContinuousDynamixelRobot(
|
||||||
|
joint_ids=self.joint_ids,
|
||||||
|
joint_offsets=self.joint_offsets,
|
||||||
|
joint_signs=self.joint_signs,
|
||||||
|
real=True,
|
||||||
|
port=port,
|
||||||
|
gripper_config=self.gripper_config,
|
||||||
|
start_joints=start_joints,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
driver_module.DynamixelDriver = original_driver
|
||||||
@ -4,6 +4,7 @@ import time
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from lerobot.utils.errors import DeviceAlreadyConnectedError, DeviceNotConnectedError
|
from lerobot.utils.errors import DeviceAlreadyConnectedError, DeviceNotConnectedError
|
||||||
from ..base_teleop import UFBaseTeleop
|
from ..base_teleop import UFBaseTeleop
|
||||||
|
from .gello_adapter import PatchedDynamixelRobotConfig
|
||||||
from .gello_teleop_config import GelloTeleopConfig
|
from .gello_teleop_config import GelloTeleopConfig
|
||||||
|
|
||||||
|
|
||||||
@ -25,8 +26,6 @@ class GelloTeleop(UFBaseTeleop):
|
|||||||
self._needs_alignment = True
|
self._needs_alignment = True
|
||||||
self._is_calibrated = True # CHECK!!
|
self._is_calibrated = True # CHECK!!
|
||||||
|
|
||||||
from gello.agents.gello_agent import DynamixelRobotConfig
|
|
||||||
|
|
||||||
joint_offsets = [0.0] * len(self.config.joint_ids)
|
joint_offsets = [0.0] * len(self.config.joint_ids)
|
||||||
self._align_gripper_to_current = self.config.gripper_open_deg is None
|
self._align_gripper_to_current = self.config.gripper_open_deg is None
|
||||||
if self.config.gripper_id >= 0:
|
if self.config.gripper_id >= 0:
|
||||||
@ -52,7 +51,7 @@ class GelloTeleop(UFBaseTeleop):
|
|||||||
"joint_offsets": joint_offsets,
|
"joint_offsets": joint_offsets,
|
||||||
"gripper_config": gripper_config
|
"gripper_config": gripper_config
|
||||||
}
|
}
|
||||||
self._dynamixel_robo_config = DynamixelRobotConfig(**param_dict)
|
self._dynamixel_robo_config = PatchedDynamixelRobotConfig(**param_dict)
|
||||||
print(self._dynamixel_robo_config)
|
print(self._dynamixel_robo_config)
|
||||||
self.dof = len(self.config.joint_ids)
|
self.dof = len(self.config.joint_ids)
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,9 @@ import pytest
|
|||||||
|
|
||||||
from lerobot_robot_ufactory.teleoperators.gello_teleop import gello_teleop as gello_module
|
from lerobot_robot_ufactory.teleoperators.gello_teleop import gello_teleop as gello_module
|
||||||
from lerobot_robot_ufactory.scripts.uf_lerobot_record import _prepare_recording_episode
|
from lerobot_robot_ufactory.scripts.uf_lerobot_record import _prepare_recording_episode
|
||||||
from gello.robots.dynamixel import DynamixelRobot
|
from lerobot_robot_ufactory.teleoperators.gello_teleop.gello_adapter import (
|
||||||
|
ContinuousDynamixelRobot,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class FakeDriver:
|
class FakeDriver:
|
||||||
@ -88,7 +90,7 @@ def test_fixed_gripper_endpoints_are_not_shifted_during_arm_alignment():
|
|||||||
|
|
||||||
|
|
||||||
def test_dynamixel_arm_joint_is_continuous_across_encoder_wrap():
|
def test_dynamixel_arm_joint_is_continuous_across_encoder_wrap():
|
||||||
robot = DynamixelRobot(
|
robot = ContinuousDynamixelRobot(
|
||||||
joint_ids=[1],
|
joint_ids=[1],
|
||||||
joint_offsets=[0.0],
|
joint_offsets=[0.0],
|
||||||
joint_signs=[1],
|
joint_signs=[1],
|
||||||
|
|||||||
1
third_party/gello_software
vendored
1
third_party/gello_software
vendored
@ -1 +0,0 @@
|
|||||||
Subproject commit 313cdd831f4e87bcbb056dbd1f68d1e8898a6681
|
|
||||||
7
uv.lock
generated
7
uv.lock
generated
@ -935,16 +935,13 @@ http = [
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "gello"
|
name = "gello"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
source = { editable = "third_party/gello_software" }
|
source = { git = "https://github.com/xArm-Developer/gello_software.git#523437fa4615155813124efeae26fdef32e546a0" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
||||||
{ name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
|
{ name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.11.*'" },
|
||||||
{ name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
|
{ name = "numpy", version = "2.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
|
||||||
requires-dist = [{ name = "numpy" }]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gitdb"
|
name = "gitdb"
|
||||||
version = "4.0.12"
|
version = "4.0.12"
|
||||||
@ -1254,7 +1251,7 @@ spacemouse = [
|
|||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "dynamixel-sdk", marker = "extra == 'gello'", specifier = ">=4.0.5" },
|
{ name = "dynamixel-sdk", marker = "extra == 'gello'", specifier = ">=4.0.5" },
|
||||||
{ name = "gello", marker = "extra == 'gello'", editable = "third_party/gello_software" },
|
{ name = "gello", marker = "extra == 'gello'", git = "https://github.com/xArm-Developer/gello_software.git" },
|
||||||
{ name = "lerobot", extras = ["intelrealsense"], specifier = "==0.4.3" },
|
{ name = "lerobot", extras = ["intelrealsense"], specifier = "==0.4.3" },
|
||||||
{ name = "mypy", marker = "extra == 'dev'", specifier = ">=1.10" },
|
{ name = "mypy", marker = "extra == 'dev'", specifier = ">=1.10" },
|
||||||
{ name = "numpy", specifier = ">=1.24" },
|
{ name = "numpy", specifier = ">=1.24" },
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user