Source code for so101_mvbench.recording.gamepad_action_generator

# SPDX-License-Identifier: MIT
"""Gamepad-driven action generator for human teleoperation.

Wraps ``gamepad_utils.JointGamepad`` in absolute mode to produce action dicts
compatible with ``LeRobotSO101Interface.real_to_sim_obs_processor()``.

Same interface as ``SineWaveActionGenerator``: call ``get_action()`` each frame
to get a dict of joint positions in LeRobot raw units.

The neutral (start) position is computed at runtime from the simulation's
init_state using ``get_raw_actions_from_radians()``, ensuring perfect alignment
with whatever USD model is loaded.
"""

from __future__ import annotations

__version__ = "1.0.0"

import logging
from importlib.resources import files as pkg_files
from pathlib import Path

import torch

from so101_mvbench.vendor.gamepad_utils import JointGamepad

logger = logging.getLogger(__name__)

# Canonical joint order from lerobot_so101_teleop
SO101_JOINT_ORDER = [
    "shoulder_pan.pos",
    "shoulder_lift.pos",
    "elbow_flex.pos",
    "wrist_flex.pos",
    "wrist_roll.pos",
    "gripper.pos",
]

# Default config shipped with gamepad_utils
_DEFAULT_CONFIG = Path(
    str(pkg_files("so101_mvbench.vendor.gamepad_utils").joinpath("configs/so101_lerobot_teleop.json"))
)


[docs] class GamepadActionGenerator: """Gamepad action generator that outputs LeRobot raw-unit joint positions. Args: coord_iface: ``LeRobotSO101Interface`` instance (provides coordinate transforms and joint range info). initial_obs_rad: Initial joint positions from ``env.reset()`` observation (policy obs tensor, in radians, shape ``[6]``). config_path: Path to gamepad JSON config. If None, uses the default ``so101_lerobot_teleop.json`` shipped with ``gamepad_utils``. """
[docs] def __init__( self, coord_iface, initial_obs_rad: torch.Tensor, config_path: str | Path | None = None, ): # Convert sim init_state (radians) -> LeRobot raw units neutral_raw: torch.Tensor = coord_iface.get_raw_actions_from_radians(initial_obs_rad) # Build neutral_pos dict for JointGamepad (keys without .pos suffix) joint_names = [j.split(".")[0] for j in SO101_JOINT_ORDER] neutral_dict = { name: neutral_raw[i].item() for i, name in enumerate(joint_names) } logger.info("GamepadActionGenerator: neutral positions (raw): %s", neutral_dict) # Resolve config path if config_path is None: config_path = _DEFAULT_CONFIG config_path = Path(config_path) logger.info("GamepadActionGenerator: config=%s", config_path) # JointGamepad in absolute mode — output values are in raw units self._gamepad = JointGamepad( config_path=config_path, sim_device="cpu", joint_order=joint_names[:5], # body joints only (gripper handled separately) neutral_pos=neutral_dict, ) logger.info("GamepadActionGenerator v%s initialized", __version__) logger.info("%s", self._gamepad)
[docs] def get_action(self) -> dict[str, float]: """Return absolute joint positions in LeRobot raw units. Returns: Dict with keys like ``"shoulder_pan.pos"`` and float values in the range [-100, 100] for body joints, [0, 100] for gripper. """ return self._gamepad.get_action()
[docs] def reset(self): """Reset all joints to the neutral (init_state) position.""" self._gamepad.reset()