From 6cc1a3df952949f2f5976ce48937a78b9cfd0b2b Mon Sep 17 00:00:00 2001 From: ChenYuhan <2514158309@qq.com> Date: Mon, 10 Aug 2026 14:06:25 +0800 Subject: [PATCH] feat: add calibrated GELLO reset workflow --- .gitmodules | 3 + config/gello/xarm7_gello_record_config.yaml | 12 +- .../gello/xarm7_gello_reset_calibration.yaml | 37 ++ pyproject.toml | 7 +- scripts/reset_gello_xarm7.py | 339 ++++++++++++++++++ .../scripts/uf_robot_teleop.py | 37 +- .../gello_teleop/gello_teleop.py | 26 +- .../gello_teleop/gello_teleop_config.py | 18 +- third_party/gello_software | 1 + uv.lock | 15 +- 10 files changed, 474 insertions(+), 21 deletions(-) create mode 100644 .gitmodules create mode 100644 config/gello/xarm7_gello_reset_calibration.yaml create mode 100644 scripts/reset_gello_xarm7.py create mode 160000 third_party/gello_software diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..73ec519 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "third_party/gello_software"] + path = third_party/gello_software + url = https://github.com/xArm-Developer/gello_software.git diff --git a/config/gello/xarm7_gello_record_config.yaml b/config/gello/xarm7_gello_record_config.yaml index 98e7137..9b35811 100644 --- a/config/gello/xarm7_gello_record_config.yaml +++ b/config/gello/xarm7_gello_record_config.yaml @@ -12,9 +12,17 @@ robot: teleop: type: uf::gello_teleop id: "gello_teleop" - port: "/dev/serial/by-id/usb-FTDI_USB__-__Serial_Converter_FTAJZYC7-if00-port0" - # GELLO calibration reference; matches the xArm SDK initial point. + port: "/dev/serial/by-id/usb-FTDI_USB__-__Serial_Converter_FTB9HYVD-if00-port0" + joint_ids: [1, 2, 3, 4, 5, 6, 7] + joint_signs: [1, 1, 1, 1, 1, 1, 1] + # Fixed from xarm7_gello_reset_calibration.yaml. The captured GELLO pose + # below maps to the xArm SDK initial point instead of recalibrating at startup. + joint_offsets: [89.472656, 239.794922, 176.396484, 155.039062, 181.669922, 126.621094, 176.132812] start_joints: [0, -30, 0, 0, 0, 30, 0] + gripper_id: 8 + gripper_open_deg: 198.017578 + gripper_close_deg: 155.75 + reset_speed_deg_s: 10.0 dataset: # root of local repo: /home//.cache/huggingface/lerobot (default) diff --git a/config/gello/xarm7_gello_reset_calibration.yaml b/config/gello/xarm7_gello_reset_calibration.yaml new file mode 100644 index 0000000..0232d70 --- /dev/null +++ b/config/gello/xarm7_gello_reset_calibration.yaml @@ -0,0 +1,37 @@ +schema_version: 1 +description: GELLO pose matching the xArm7 SDK initial point +robot_initial_joints_deg: +- 0.0 +- -30.0 +- 0.0 +- 0.0 +- 0.0 +- 30.0 +- 0.0 +dynamixel_ids: +- 1 +- 2 +- 3 +- 4 +- 5 +- 6 +- 7 +- 8 +target_raw_counts: +- 1018 +- 2387 +- 2007 +- 1764 +- 2067 +- 1782 +- 2004 +- 2253 +target_encoder_deg: +- 89.472656 +- 209.794922 +- 176.396484 +- 155.039062 +- 181.669922 +- 156.621094 +- 176.132812 +- 198.017578 diff --git a/pyproject.toml b/pyproject.toml index 9d356a0..6f97f85 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,8 +45,8 @@ uf-camera-test = "lerobot_robot_ufactory.scripts.uf_camera_test:main" [project.optional-dependencies] # GELLO 遥操作 gello = [ - "gello @ git+https://github.com/xArm-Developer/gello_software.git", - "dynamixel-sdk @ git+https://github.com/ROBOTIS-GIT/DynamixelSDK.git#subdirectory=python", + "gello", + "dynamixel-sdk>=4.0.5", ] # SpaceMouse 遥操作 spacemouse = [ @@ -70,6 +70,9 @@ where = ["src"] [tool.setuptools.package-data] "lerobot_robot_ufactory.devices.umi.xvlib" = ["*.so", "*.so.*"] +[tool.uv.sources] +gello = { path = "third_party/gello_software", editable = true } + [tool.ruff] line-length = 100 target-version = "py310" diff --git a/scripts/reset_gello_xarm7.py b/scripts/reset_gello_xarm7.py new file mode 100644 index 0000000..a082d85 --- /dev/null +++ b/scripts/reset_gello_xarm7.py @@ -0,0 +1,339 @@ +#!/usr/bin/env python3 +"""Calibrate and reset a GELLO leader to the xArm7 SDK initial pose.""" + +from __future__ import annotations + +import argparse +import time +from pathlib import Path +from typing import Sequence + +import numpy as np +import yaml +from dynamixel_sdk import COMM_SUCCESS, GroupSyncWrite, PacketHandler, PortHandler + + +DEFAULT_PORT = ( + "/dev/serial/by-id/" + "usb-FTDI_USB__-__Serial_Converter_FTB9HYVD-if00-port0" +) +DEFAULT_CALIBRATION = Path("config/gello/xarm7_gello_reset_calibration.yaml") +ROBOT_INITIAL_JOINTS_DEG = [0.0, -30.0, 0.0, 0.0, 0.0, 30.0, 0.0] +JOINT_IDS = list(range(1, 8)) +GRIPPER_ID = 8 +BAUDRATE = 57600 + +ADDR_OPERATING_MODE = 11 +ADDR_TORQUE_ENABLE = 64 +ADDR_HARDWARE_ERROR = 70 +ADDR_GOAL_POSITION = 116 +ADDR_PRESENT_CURRENT = 126 +ADDR_PRESENT_POSITION = 132 +POSITION_CONTROL_MODE = 3 + + +class GelloBus: + def __init__(self, port: str, ids: Sequence[int]) -> None: + self.ids = list(ids) + self.port = PortHandler(port) + self.packet = PacketHandler(2.0) + self.writer = GroupSyncWrite( + self.port, self.packet, ADDR_GOAL_POSITION, 4 + ) + + if not self.port.openPort(): + raise RuntimeError(f"Failed to open GELLO port: {port}") + if not self.port.setBaudRate(BAUDRATE): + self.port.closePort() + raise RuntimeError(f"Failed to set GELLO baud rate to {BAUDRATE}") + + def close(self) -> None: + self.writer.clearParam() + self.port.closePort() + + def _check(self, operation: str, dxl_id: int, comm: int, error: int) -> None: + if comm != COMM_SUCCESS: + detail = self.packet.getTxRxResult(comm) + raise RuntimeError( + f"{operation} failed for Dynamixel {dxl_id}: {detail} ({comm})" + ) + if error != 0: + detail = self.packet.getRxPacketError(error) + raise RuntimeError( + f"{operation} failed for Dynamixel {dxl_id}: {detail} ({error})" + ) + + def read_u8(self, dxl_id: int, address: int) -> int: + value, comm, error = self.packet.read1ByteTxRx( + self.port, dxl_id, address + ) + self._check("read", dxl_id, comm, error) + return value + + def read_i16(self, dxl_id: int, address: int) -> int: + value, comm, error = self.packet.read2ByteTxRx( + self.port, dxl_id, address + ) + self._check("read", dxl_id, comm, error) + return value - 0x10000 if value >= 0x8000 else value + + def read_i32(self, dxl_id: int, address: int) -> int: + value, comm, error = self.packet.read4ByteTxRx( + self.port, dxl_id, address + ) + self._check("read", dxl_id, comm, error) + return value - 0x100000000 if value >= 0x80000000 else value + + def positions(self) -> np.ndarray: + return np.asarray( + [self.read_i32(dxl_id, ADDR_PRESENT_POSITION) for dxl_id in self.ids], + dtype=float, + ) + + def set_torque(self, enabled: bool) -> None: + value = 1 if enabled else 0 + for dxl_id in self.ids: + comm, error = self.packet.write1ByteTxRx( + self.port, dxl_id, ADDR_TORQUE_ENABLE, value + ) + self._check("set torque", dxl_id, comm, error) + + def verify_position_mode(self) -> None: + for dxl_id in self.ids: + mode = self.read_u8(dxl_id, ADDR_OPERATING_MODE) + if mode != POSITION_CONTROL_MODE: + raise RuntimeError( + f"Dynamixel {dxl_id} is in mode {mode}, expected position mode 3" + ) + + def write_positions(self, raw_positions: Sequence[int]) -> None: + self.writer.clearParam() + try: + for dxl_id, raw_position in zip(self.ids, raw_positions, strict=True): + encoded = int(raw_position) & 0xFFFFFFFF + data = list(encoded.to_bytes(4, byteorder="little", signed=False)) + if not self.writer.addParam(dxl_id, data): + raise RuntimeError( + f"Failed to add goal position for Dynamixel {dxl_id}" + ) + comm = self.writer.txPacket() + if comm != COMM_SUCCESS: + detail = self.packet.getTxRxResult(comm) + raise RuntimeError( + f"SyncWrite failed: {detail} ({comm})" + ) + finally: + self.writer.clearParam() + + +def counts_to_degrees(values: Sequence[float]) -> np.ndarray: + return np.asarray(values, dtype=float) / 4096.0 * 360.0 + + +def save_calibration(path: Path, ids: Sequence[int], targets: Sequence[int]) -> None: + data = { + "schema_version": 1, + "description": "GELLO pose matching the xArm7 SDK initial point", + "robot_initial_joints_deg": ROBOT_INITIAL_JOINTS_DEG, + "dynamixel_ids": list(ids), + "target_raw_counts": [int(value) for value in targets], + "target_encoder_deg": [ + round(float(value), 6) for value in counts_to_degrees(targets) + ], + } + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(yaml.safe_dump(data, sort_keys=False), encoding="utf-8") + + +def load_calibration(path: Path, expected_ids: Sequence[int]) -> np.ndarray: + if not path.is_file(): + raise FileNotFoundError( + f"Calibration file not found: {path}. Run this script with --calibrate first." + ) + data = yaml.safe_load(path.read_text(encoding="utf-8")) + ids = data.get("dynamixel_ids") + targets = data.get("target_raw_counts") + if ids != list(expected_ids): + raise ValueError(f"Calibration IDs {ids} do not match expected IDs {expected_ids}") + if not isinstance(targets, list) or len(targets) != len(expected_ids): + raise ValueError("Calibration target_raw_counts has an invalid length") + return np.asarray(targets, dtype=float) + + +def calibrate(args: argparse.Namespace) -> None: + ids = JOINT_IDS + ([GRIPPER_ID] if not args.no_gripper else []) + bus = GelloBus(args.port, ids) + try: + bus.set_torque(False) + print("GELLO torque is disabled.") + if not args.yes: + print( + "Manually place GELLO in the physical pose matching xArm7 " + f"{ROBOT_INITIAL_JOINTS_DEG} degrees." + ) + print("Keep the gripper open, then press Enter to capture this pose.") + input() + targets = np.rint(bus.positions()).astype(int) + save_calibration(args.calibration, ids, targets) + print(f"Saved calibration: {args.calibration}") + print("Target encoder degrees:", counts_to_degrees(targets).round(2).tolist()) + finally: + try: + bus.set_torque(False) + finally: + bus.close() + + +def reset(args: argparse.Namespace) -> None: + ids = JOINT_IDS + ([GRIPPER_ID] if not args.no_gripper else []) + targets = load_calibration(args.calibration, ids) + bus = GelloBus(args.port, ids) + torque_enabled = False + completed = False + try: + bus.set_torque(False) + bus.verify_position_mode() + current = bus.positions() + move_deg = counts_to_degrees(targets - current) + max_move_deg = float(np.max(np.abs(move_deg))) + print("Current encoder degrees:", counts_to_degrees(current).round(2).tolist()) + print("Target encoder degrees: ", counts_to_degrees(targets).round(2).tolist()) + print("Required move degrees: ", move_deg.round(2).tolist()) + print(f"Maximum move: {max_move_deg:.2f} degrees") + + if max_move_deg > args.max_move_deg: + raise RuntimeError( + f"Refusing reset: {max_move_deg:.2f} degree move exceeds " + f"--max-move-deg={args.max_move_deg:.2f}" + ) + if not args.yes: + answer = input("Type 'yes' to enable GELLO torque and reset: ").strip() + if answer.lower() != "yes": + print("Reset cancelled.") + return + + bus.write_positions(np.rint(current).astype(int)) + for remaining in (3, 2, 1): + print(f"Enabling torque in {remaining}...") + time.sleep(1.0) + bus.set_torque(True) + torque_enabled = True + + duration = max(0.5, max_move_deg / args.speed_deg_s) + steps = max(1, int(duration * args.control_hz)) + started = time.monotonic() + for step in range(1, steps + 1): + u = step / steps + smooth = u * u * (3.0 - 2.0 * u) + command = np.rint(current + (targets - current) * smooth).astype(int) + bus.write_positions(command) + + if step % max(1, int(args.control_hz / 5)) == 0: + currents = [ + bus.read_i16(dxl_id, ADDR_PRESENT_CURRENT) for dxl_id in ids + ] + errors = [ + bus.read_u8(dxl_id, ADDR_HARDWARE_ERROR) for dxl_id in ids + ] + if any(errors): + raise RuntimeError(f"Dynamixel hardware errors: {errors}") + if max(abs(value) for value in currents) > args.max_current_raw: + raise RuntimeError( + f"Current safety threshold exceeded: {currents}" + ) + + delay = started + step / args.control_hz - time.monotonic() + if delay > 0: + time.sleep(delay) + + deadline = time.monotonic() + args.settle_timeout_s + final = bus.positions() + while time.monotonic() < deadline: + error_deg = counts_to_degrees(final - targets) + if float(np.max(np.abs(error_deg))) <= args.tolerance_deg: + break + bus.write_positions(np.rint(targets).astype(int)) + time.sleep(1.0 / args.control_hz) + final = bus.positions() + else: + error_deg = counts_to_degrees(final - targets) + raise RuntimeError( + "GELLO did not reach the calibrated pose; final errors: " + f"{error_deg.round(2).tolist()} degrees" + ) + + print("Final encoder degrees:", counts_to_degrees(final).round(2).tolist()) + print("GELLO reset completed.") + completed = True + finally: + if torque_enabled and (not completed or args.release_after_reset): + try: + bus.set_torque(False) + print("GELLO torque disabled.") + except Exception as exc: + print(f"WARNING: failed to disable GELLO torque: {exc}") + bus.close() + + if completed and not args.release_after_reset: + print("GELLO torque remains enabled and is holding the calibrated pose.") + + +def release(args: argparse.Namespace) -> None: + ids = JOINT_IDS + ([GRIPPER_ID] if not args.no_gripper else []) + bus = GelloBus(args.port, ids) + try: + bus.set_torque(False) + print("GELLO torque disabled.") + finally: + bus.close() + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description=( + "Calibrate or reset GELLO to the physical pose matching the xArm7 " + "SDK initial joints [0, -30, 0, 0, 0, 30, 0] degrees." + ) + ) + parser.add_argument("--port", default=DEFAULT_PORT) + parser.add_argument("--calibration", type=Path, default=DEFAULT_CALIBRATION) + mode = parser.add_mutually_exclusive_group() + mode.add_argument("--calibrate", action="store_true") + mode.add_argument( + "--release", + action="store_true", + help="Disable GELLO torque without moving it", + ) + parser.add_argument("--no-gripper", action="store_true") + parser.add_argument("--yes", action="store_true", help="Skip interactive confirmation") + parser.add_argument("--speed-deg-s", type=float, default=20.0) + parser.add_argument("--control-hz", type=float, default=50.0) + parser.add_argument("--max-move-deg", type=float, default=90.0) + parser.add_argument("--max-current-raw", type=int, default=300) + parser.add_argument("--tolerance-deg", type=float, default=3.0) + parser.add_argument("--settle-timeout-s", type=float, default=5.0) + parser.add_argument( + "--release-after-reset", + action="store_true", + help="Disable torque after a successful reset instead of holding the pose", + ) + args = parser.parse_args() + if args.speed_deg_s <= 0 or args.control_hz <= 0: + parser.error("--speed-deg-s and --control-hz must be positive") + if args.max_move_deg <= 0 or args.tolerance_deg <= 0: + parser.error("--max-move-deg and --tolerance-deg must be positive") + return args + + +def main() -> None: + args = parse_args() + if args.calibrate: + calibrate(args) + elif args.release: + release(args) + else: + reset(args) + + +if __name__ == "__main__": + main() diff --git a/src/lerobot_robot_ufactory/scripts/uf_robot_teleop.py b/src/lerobot_robot_ufactory/scripts/uf_robot_teleop.py index 77ee937..bbe7ad7 100644 --- a/src/lerobot_robot_ufactory/scripts/uf_robot_teleop.py +++ b/src/lerobot_robot_ufactory/scripts/uf_robot_teleop.py @@ -1,5 +1,6 @@ import sys import argparse +import atexit import logging import time from pathlib import Path @@ -53,8 +54,37 @@ def teleop_loop(cfg: TeleopConfig): teleop_action_processor, robot_action_processor, robot_observation_processor = make_default_processors() + robot_connected = False + teleop_connected = False + listener = None + cleanup_done = False + + def cleanup_connections(): + nonlocal cleanup_done + if cleanup_done: + return + cleanup_done = True + if teleop_connected: + try: + teleop.disconnect() + except Exception: + logging.exception("Failed to disconnect teleoperator cleanly") + if robot_connected: + try: + robot.disconnect() + except Exception: + logging.exception("Failed to disconnect robot cleanly") + if listener is not None: + try: + listener.stop() + except Exception: + logging.exception("Failed to stop keyboard listener cleanly") + + atexit.register(cleanup_connections) robot.connect() + robot_connected = True teleop.connect() + teleop_connected = True sleep_time_s = 1 / cfg.fps @@ -77,7 +107,6 @@ def teleop_loop(cfg: TeleopConfig): is_reset = is_uf_teleop is_paused = True events = {"exit": False} - listener = None key_dict = {} if is_evt: @@ -185,10 +214,8 @@ def teleop_loop(cfg: TeleopConfig): precise_sleep(sleep_time_s - dt_s) print("\n********** Teleop Control Loop Exit **********") - robot.disconnect() - teleop.disconnect() - if is_evt and listener is not None: - listener.stop() + cleanup_connections() + atexit.unregister(cleanup_connections) @parser.wrap() def get_cfg(cfg: TeleopConfig) -> TeleopConfig: diff --git a/src/lerobot_robot_ufactory/teleoperators/gello_teleop/gello_teleop.py b/src/lerobot_robot_ufactory/teleoperators/gello_teleop/gello_teleop.py index 2f5bc44..0bfc2b5 100644 --- a/src/lerobot_robot_ufactory/teleoperators/gello_teleop/gello_teleop.py +++ b/src/lerobot_robot_ufactory/teleoperators/gello_teleop/gello_teleop.py @@ -10,7 +10,6 @@ from .gello_teleop_config import GelloTeleopConfig logger = logging.getLogger(__name__) -GELLO_RESET_SPEED_DEG = 30.0 GELLO_RESET_TOLERANCE_DEG = 2.0 GELLO_RESET_CONTROL_HZ = 50.0 GELLO_RESET_TIMEOUT_MARGIN_S = 5.0 @@ -44,13 +43,26 @@ class GelloTeleop(UFBaseTeleop): driver.get_joints() # warmup curr_joints = driver.get_joints() driver.close() - joint_offsets = [] start_joints = list(map(math.radians, self.config.start_joints)) - for i in range(len(start_joints)): - offset = curr_joints[i] - start_joints[i] / self.config.joint_signs[i] - joint_offsets.append(offset) + if self.config.joint_offsets is not None: + joint_offsets = list(map(math.radians, self.config.joint_offsets)) + else: + joint_offsets = [] + for i in range(len(start_joints)): + offset = curr_joints[i] - start_joints[i] / self.config.joint_signs[i] + joint_offsets.append(offset) if self.config.gripper_id >= 0: - gripper_config = [self.config.gripper_id, np.rad2deg(curr_joints[-1]) - 0.2, np.rad2deg(curr_joints[-1]) - 42] + if self.config.gripper_open_deg is not None: + gripper_open_deg = self.config.gripper_open_deg + gripper_close_deg = self.config.gripper_close_deg + else: + gripper_open_deg = np.rad2deg(curr_joints[-1]) - 0.2 + gripper_close_deg = np.rad2deg(curr_joints[-1]) - 42 + gripper_config = [ + self.config.gripper_id, + gripper_open_deg, + gripper_close_deg, + ] else: gripper_config = None @@ -150,7 +162,7 @@ class GelloTeleop(UFBaseTeleop): target_raw[self.dof] = gripper_open + gripper_pos * (gripper_close - gripper_open) arm_delta = np.max(np.abs(target_raw[: self.dof] - current_raw[: self.dof])) - reset_speed_rad_s = math.radians(GELLO_RESET_SPEED_DEG) + reset_speed_rad_s = math.radians(self.config.reset_speed_deg_s) duration_s = max(0.5, float(arm_delta / reset_speed_rad_s)) deadline = time.perf_counter() + duration_s + GELLO_RESET_TIMEOUT_MARGIN_S success = False diff --git a/src/lerobot_robot_ufactory/teleoperators/gello_teleop/gello_teleop_config.py b/src/lerobot_robot_ufactory/teleoperators/gello_teleop/gello_teleop_config.py index e0b2ab5..9e4173f 100644 --- a/src/lerobot_robot_ufactory/teleoperators/gello_teleop/gello_teleop_config.py +++ b/src/lerobot_robot_ufactory/teleoperators/gello_teleop/gello_teleop_config.py @@ -1,7 +1,7 @@ #!/usr/bin/env python from dataclasses import dataclass -from typing import Tuple +from typing import Optional, Tuple from lerobot.teleoperators import TeleoperatorConfig @@ -14,10 +14,26 @@ class GelloTeleopConfig(TeleoperatorConfig): # Others: Calibration angles, joint directions etc joint_ids: Tuple[int, ...] = (1, 2, 3, 4, 5, 6, 7) joint_signs: Tuple[int, ...] = (1, 1, 1, 1, 1, 1, 1) # if follow the original open-sourced gello xarm7 setup + # Raw Dynamixel zero offsets in degrees. When omitted, the current GELLO + # pose is treated as start_joints for backwards compatibility. + joint_offsets: Optional[Tuple[float, ...]] = None # GELLO encoder calibration reference; this is not the xArm reset target. start_joints: Tuple[float, ...] = (0, 0, 0, 90, 0, 90, 0) # ° gripper_id: int = 8 # -1: no gripper + gripper_open_deg: Optional[float] = None + gripper_close_deg: Optional[float] = None + reset_speed_deg_s: float = 10.0 torque_joint_ids: Tuple[int, ...] = None # deprecated; reset controls all GELLO joints. def __post_init__(self): self.id = 'gello_teleop' if self.id is None else self.id + if len(self.joint_ids) != len(self.joint_signs): + raise ValueError("joint_ids and joint_signs must have the same length") + if len(self.joint_ids) != len(self.start_joints): + raise ValueError("joint_ids and start_joints must have the same length") + if self.joint_offsets is not None and len(self.joint_ids) != len(self.joint_offsets): + raise ValueError("joint_ids and joint_offsets must have the same length") + if (self.gripper_open_deg is None) != (self.gripper_close_deg is None): + raise ValueError("gripper_open_deg and gripper_close_deg must be set together") + if self.reset_speed_deg_s <= 0: + raise ValueError("reset_speed_deg_s must be positive") diff --git a/third_party/gello_software b/third_party/gello_software new file mode 160000 index 0000000..b543065 --- /dev/null +++ b/third_party/gello_software @@ -0,0 +1 @@ +Subproject commit b543065ca36f7a444f7f5d110e49b8146d7e1cf3 diff --git a/uv.lock b/uv.lock index ce8071d..9874c76 100644 --- a/uv.lock +++ b/uv.lock @@ -743,10 +743,14 @@ wheels = [ [[package]] name = "dynamixel-sdk" version = "4.0.5" -source = { git = "https://github.com/ROBOTIS-GIT/DynamixelSDK.git?subdirectory=python#2ded684dff05a40ac78d6a16105c6ddc1b3b9930" } +source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyserial" }, ] +sdist = { url = "https://files.pythonhosted.org/packages/de/ad/05bb6c7fe54c01d2712398872b300891a5b6a0181e69335f4e1717d72805/dynamixel_sdk-4.0.5.tar.gz", hash = "sha256:498ba2090f5f9844ac0610553cc70b8c79e3f6f52f7911425cdb2857210b9630", size = 29695, upload-time = "2026-05-06T02:12:08.389Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a5/319d15afd31997e54e5c88b2fe1d53d15c9e63c3b8d51eda40ddff629443/dynamixel_sdk-4.0.5-py3-none-any.whl", hash = "sha256:36f9c0c078cbb8e87f5413bfcf76da8f50ce07d17690c52e52ad0f0180a7d6d8", size = 103493, upload-time = "2026-05-06T02:12:06.77Z" }, +] [[package]] name = "einops" @@ -931,13 +935,16 @@ http = [ [[package]] name = "gello" version = "0.0.1" -source = { git = "https://github.com/xArm-Developer/gello_software.git#523437fa4615155813124efeae26fdef32e546a0" } +source = { editable = "third_party/gello_software" } dependencies = [ { 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.5.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] +[package.metadata] +requires-dist = [{ name = "numpy" }] + [[package]] name = "gitdb" version = "4.0.12" @@ -1246,8 +1253,8 @@ spacemouse = [ [package.metadata] requires-dist = [ - { name = "dynamixel-sdk", marker = "extra == 'gello'", git = "https://github.com/ROBOTIS-GIT/DynamixelSDK.git?subdirectory=python" }, - { name = "gello", marker = "extra == 'gello'", git = "https://github.com/xArm-Developer/gello_software.git" }, + { name = "dynamixel-sdk", marker = "extra == 'gello'", specifier = ">=4.0.5" }, + { name = "gello", marker = "extra == 'gello'", editable = "third_party/gello_software" }, { name = "lerobot", extras = ["intelrealsense"], specifier = "==0.4.3" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.10" }, { name = "numpy", specifier = ">=1.24" },