Commit c45b8c0b authored by rwiltz's avatar rwiltz Committed by Kelly Guo

Implements teleop config parameters and device factory (#293)

This change moves the configuration of the teleop device from the end
user scripts (record_demos.py, teleop_se3.py, etc) to the env config
itself. This allows the task to specify what teleop devices are
supported along with the per env configuration of supported teleop
device.

Example teleop config inside of the env config:
```
        self.teleop_devices = DevicesCfg(
            devices={
                "handtracking": OpenXRDeviceCfg(
                    retargeters=[
                        Se3RelRetargeterCfg(
                            bound_hand=OpenXRDevice.TrackingTarget.HAND_RIGHT,
                            zero_out_xy_rotation=True,
                            use_wrist_rotation=False,
                            use_wrist_position=True,
                            delta_pos_scale_factor=10.0,
                            delta_rot_scale_factor=10.0,
                            sim_device=self.sim.device,
                        ),
                        GripperRetargeterCfg(
                            bound_hand=OpenXRDevice.TrackingTarget.HAND_RIGHT, sim_device=self.sim.device
                        ),
                    ],
                    sim_device=self.sim.device,
                    xr_cfg=self.xr,
                ),
                "keyboard": Se3KeyboardCfg(
                    pos_sensitivity=0.05,
                    rot_sensitivity=0.05,
                    sim_device=self.sim.device,
                ),
            }
        )
```
<!--
Thank you for your interest in sending a pull request. Please make sure
to check the contribution guidelines.

Link:
https://isaac-sim.github.io/IsaacLab/main/source/refs/contributing.html
-->

Please include a summary of the change and which issue is fixed. Please
also include relevant motivation and context.
List any dependencies that are required for this change.

Fixes # (issue)

<!-- As a practice, it is recommended to open an issue to have
discussions on the proposed pull request.
This makes it easier for the community to keep track of what is being
developed or added, and if a given feature
is demanded by more than one party. -->

<!-- As you go through the list, delete the ones that are not
applicable. -->

- New feature (non-breaking change which adds functionality)
- This change requires a documentation update

Please attach before and after screenshots of the change if applicable.

<!--
Example:

| Before | After |
| ------ | ----- |
| _gif/png before_ | _gif/png after_ |

To upload images to a PR -- simply drag and drop an image while in edit
mode and it should upload the image directly. You can then paste that
source into the above before/after sections.
-->

- [x] I have run the [`pre-commit` checks](https://pre-commit.com/) with
`./isaaclab.sh --format`
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] I have updated the changelog and the corresponding version in the
extension's `config/extension.toml` file
- [x] I have added my name to the `CONTRIBUTORS.md` or my name already
exists there

<!--
As you go through the checklist above, you can mark something as done by
putting an x character in it

For example,
- [x] I have done this task
- [ ] I have not done this task
-->

---------
Signed-off-by: 's avatarKelly Guo <kellyg@nvidia.com>
Co-authored-by: 's avatarKelly Guo <kellyguo123@hotmail.com>
Co-authored-by: 's avatarKelly Guo <kellyg@nvidia.com>
parent 0d0dd13f
...@@ -62,7 +62,7 @@ if args_cli.enable_pinocchio: ...@@ -62,7 +62,7 @@ if args_cli.enable_pinocchio:
# Only enables inputs if this script is NOT headless mode # Only enables inputs if this script is NOT headless mode
if not args_cli.headless and not os.environ.get("HEADLESS", 0): if not args_cli.headless and not os.environ.get("HEADLESS", 0):
from isaaclab.devices import Se3Keyboard from isaaclab.devices import Se3Keyboard, Se3KeyboardCfg
from isaaclab.envs import ManagerBasedRLMimicEnv from isaaclab.envs import ManagerBasedRLMimicEnv
from isaaclab.envs.mdp.recorders.recorders_cfg import ActionStateRecorderManagerCfg from isaaclab.envs.mdp.recorders.recorders_cfg import ActionStateRecorderManagerCfg
...@@ -225,7 +225,7 @@ def main(): ...@@ -225,7 +225,7 @@ def main():
# Only enables inputs if this script is NOT headless mode # Only enables inputs if this script is NOT headless mode
if not args_cli.headless and not os.environ.get("HEADLESS", 0): if not args_cli.headless and not os.environ.get("HEADLESS", 0):
keyboard_interface = Se3Keyboard(pos_sensitivity=0.1, rot_sensitivity=0.1) keyboard_interface = Se3Keyboard(Se3KeyboardCfg(pos_sensitivity=0.1, rot_sensitivity=0.1))
keyboard_interface.add_callback("N", play_cb) keyboard_interface.add_callback("N", play_cb)
keyboard_interface.add_callback("B", pause_cb) keyboard_interface.add_callback("B", pause_cb)
keyboard_interface.add_callback("Q", skip_episode_cb) keyboard_interface.add_callback("Q", skip_episode_cb)
......
...@@ -80,7 +80,7 @@ import random ...@@ -80,7 +80,7 @@ import random
import time import time
import torch import torch
from isaaclab.devices import Se3Keyboard, Se3SpaceMouse from isaaclab.devices import Se3Keyboard, Se3KeyboardCfg, Se3SpaceMouse, Se3SpaceMouseCfg
from isaaclab.envs import ManagerBasedRLMimicEnv from isaaclab.envs import ManagerBasedRLMimicEnv
from isaaclab.envs.mdp.recorders.recorders_cfg import ActionStateRecorderManagerCfg from isaaclab.envs.mdp.recorders.recorders_cfg import ActionStateRecorderManagerCfg
from isaaclab.managers import DatasetExportMode, RecorderTerm, RecorderTermCfg from isaaclab.managers import DatasetExportMode, RecorderTerm, RecorderTermCfg
...@@ -198,9 +198,9 @@ async def run_teleop_robot( ...@@ -198,9 +198,9 @@ async def run_teleop_robot(
# create controller if needed # create controller if needed
if teleop_interface is None: if teleop_interface is None:
if args_cli.teleop_device.lower() == "keyboard": if args_cli.teleop_device.lower() == "keyboard":
teleop_interface = Se3Keyboard(pos_sensitivity=0.2, rot_sensitivity=0.5) teleop_interface = Se3Keyboard(Se3KeyboardCfg(pos_sensitivity=0.2, rot_sensitivity=0.5))
elif args_cli.teleop_device.lower() == "spacemouse": elif args_cli.teleop_device.lower() == "spacemouse":
teleop_interface = Se3SpaceMouse(pos_sensitivity=0.2, rot_sensitivity=0.5) teleop_interface = Se3SpaceMouse(Se3SpaceMouseCfg(pos_sensitivity=0.2, rot_sensitivity=0.5))
else: else:
raise ValueError( raise ValueError(
f"Invalid device interface '{args_cli.teleop_device}'. Supported: 'keyboard', 'spacemouse'." f"Invalid device interface '{args_cli.teleop_device}'. Supported: 'keyboard', 'spacemouse'."
......
This diff is collapsed.
...@@ -61,7 +61,7 @@ import gymnasium as gym ...@@ -61,7 +61,7 @@ import gymnasium as gym
import os import os
import torch import torch
from isaaclab.devices import Se3Keyboard from isaaclab.devices import Se3Keyboard, Se3KeyboardCfg
from isaaclab.utils.datasets import EpisodeData, HDF5DatasetFileHandler from isaaclab.utils.datasets import EpisodeData, HDF5DatasetFileHandler
if args_cli.enable_pinocchio: if args_cli.enable_pinocchio:
...@@ -149,7 +149,7 @@ def main(): ...@@ -149,7 +149,7 @@ def main():
# create environment from loaded config # create environment from loaded config
env = gym.make(args_cli.task, cfg=env_cfg).unwrapped env = gym.make(args_cli.task, cfg=env_cfg).unwrapped
teleop_interface = Se3Keyboard(pos_sensitivity=0.1, rot_sensitivity=0.1) teleop_interface = Se3Keyboard(Se3KeyboardCfg(pos_sensitivity=0.1, rot_sensitivity=0.1))
teleop_interface.add_callback("N", play_cb) teleop_interface.add_callback("N", play_cb)
teleop_interface.add_callback("B", pause_cb) teleop_interface.add_callback("B", pause_cb)
print('Press "B" to pause and "N" to resume the replayed actions.') print('Press "B" to pause and "N" to resume the replayed actions.')
......
...@@ -376,7 +376,7 @@ Added ...@@ -376,7 +376,7 @@ Added
~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~
Added Added
~~~~~ ^^^^^
* Added the :meth:`~isaaclab.env.mdp.observations.joint_effort` * Added the :meth:`~isaaclab.env.mdp.observations.joint_effort`
...@@ -391,6 +391,17 @@ Added ...@@ -391,6 +391,17 @@ Added
* Added :meth:`~isaaclab.envs.mdp.observations.body_projected_gravity_b` * Added :meth:`~isaaclab.envs.mdp.observations.body_projected_gravity_b`
0.37.5 (2025-05-12)
~~~~~~~~~~~~~~~~~~~
Added
^^^^^
* Added a new teleop configuration class :class:`~isaaclab.devices.DevicesCfg` to support multiple teleoperation
devices declared in the environment configuration file.
* Implemented a factory function to create teleoperation devices based on the device configuration.
0.37.4 (2025-05-12) 0.37.4 (2025-05-12)
~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~
......
...@@ -590,7 +590,7 @@ class AppLauncher: ...@@ -590,7 +590,7 @@ class AppLauncher:
def _resolve_xr_settings(self, launcher_args: dict): def _resolve_xr_settings(self, launcher_args: dict):
"""Resolve XR related settings.""" """Resolve XR related settings."""
xr_env = int(os.environ.get("XR", 0)) xr_env = int(os.environ.get("XR", 0))
xr_arg = launcher_args.pop("xr", AppLauncher._APPLAUNCHER_CFG_INFO["xr"][1]) xr_arg = launcher_args.get("xr", AppLauncher._APPLAUNCHER_CFG_INFO["xr"][1])
xr_valid_vals = {0, 1} xr_valid_vals = {0, 1}
if xr_env not in xr_valid_vals: if xr_env not in xr_valid_vals:
raise ValueError(f"Invalid value for environment variable `XR`: {xr_env} .Expected: {xr_valid_vals} .") raise ValueError(f"Invalid value for environment variable `XR`: {xr_env} .Expected: {xr_valid_vals} .")
......
...@@ -19,9 +19,10 @@ to add user-defined callback functions to be called when a particular input is p ...@@ -19,9 +19,10 @@ to add user-defined callback functions to be called when a particular input is p
the peripheral device. the peripheral device.
""" """
from .device_base import DeviceBase from .device_base import DeviceBase, DeviceCfg, DevicesCfg
from .gamepad import Se2Gamepad, Se3Gamepad from .gamepad import Se2Gamepad, Se2GamepadCfg, Se3Gamepad, Se3GamepadCfg
from .keyboard import Se2Keyboard, Se3Keyboard from .keyboard import Se2Keyboard, Se2KeyboardCfg, Se3Keyboard, Se3KeyboardCfg
from .openxr import OpenXRDevice from .openxr import OpenXRDevice, OpenXRDeviceCfg
from .retargeter_base import RetargeterBase from .retargeter_base import RetargeterBase, RetargeterCfg
from .spacemouse import Se2SpaceMouse, Se3SpaceMouse from .spacemouse import Se2SpaceMouse, Se2SpaceMouseCfg, Se3SpaceMouse, Se3SpaceMouseCfg
from .teleop_device_factory import create_teleop_device
...@@ -5,11 +5,28 @@ ...@@ -5,11 +5,28 @@
"""Base class for teleoperation interface.""" """Base class for teleoperation interface."""
import torch
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass, field
from typing import Any from typing import Any
from isaaclab.devices.retargeter_base import RetargeterBase from isaaclab.devices.retargeter_base import RetargeterBase, RetargeterCfg
@dataclass
class DeviceCfg:
"""Configuration for teleoperation devices."""
sim_device: str = "cpu"
retargeters: list[RetargeterCfg] = field(default_factory=list)
@dataclass
class DevicesCfg:
"""Configuration for all supported teleoperation devices."""
devices: dict[str, DeviceCfg] = field(default_factory=dict)
class DeviceBase(ABC): class DeviceBase(ABC):
...@@ -76,7 +93,7 @@ class DeviceBase(ABC): ...@@ -76,7 +93,7 @@ class DeviceBase(ABC):
""" """
raise NotImplementedError("Derived class must implement _get_raw_data() or override advance()") raise NotImplementedError("Derived class must implement _get_raw_data() or override advance()")
def advance(self) -> Any: def advance(self) -> torch.Tensor:
"""Process current device state and return control commands. """Process current device state and return control commands.
This method retrieves raw data from the device and optionally applies This method retrieves raw data from the device and optionally applies
...@@ -87,8 +104,9 @@ class DeviceBase(ABC): ...@@ -87,8 +104,9 @@ class DeviceBase(ABC):
2. Override this method completely for custom command processing 2. Override this method completely for custom command processing
Returns: Returns:
Raw device data if no retargeters are configured. When no retargeters are configured, returns raw device data in its native format.
When retargeters are configured, returns a tuple containing each retargeter's processed output. When retargeters are configured, returns a torch.Tensor containing the concatenated
outputs from all retargeters.
""" """
raw_data = self._get_raw_data() raw_data = self._get_raw_data()
...@@ -97,4 +115,5 @@ class DeviceBase(ABC): ...@@ -97,4 +115,5 @@ class DeviceBase(ABC):
return raw_data return raw_data
# With multiple retargeters, return a tuple of outputs # With multiple retargeters, return a tuple of outputs
return tuple(retargeter.retarget(raw_data) for retargeter in self._retargeters) # Concatenate retargeted outputs into a single tensor
return torch.cat([retargeter.retarget(raw_data) for retargeter in self._retargeters], dim=-1)
...@@ -5,5 +5,5 @@ ...@@ -5,5 +5,5 @@
"""Gamepad device for SE(2) and SE(3) control.""" """Gamepad device for SE(2) and SE(3) control."""
from .se2_gamepad import Se2Gamepad from .se2_gamepad import Se2Gamepad, Se2GamepadCfg
from .se3_gamepad import Se3Gamepad from .se3_gamepad import Se3Gamepad, Se3GamepadCfg
...@@ -6,14 +6,26 @@ ...@@ -6,14 +6,26 @@
"""Gamepad controller for SE(2) control.""" """Gamepad controller for SE(2) control."""
import numpy as np import numpy as np
import torch
import weakref import weakref
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass
import carb import carb
import carb.input import carb.input
import omni import omni
from ..device_base import DeviceBase from ..device_base import DeviceBase, DeviceCfg
@dataclass
class Se2GamepadCfg(DeviceCfg):
"""Configuration for SE2 gamepad devices."""
v_x_sensitivity: float = 1.0
v_y_sensitivity: float = 1.0
omega_z_sensitivity: float = 1.0
dead_zone: float = 0.01
class Se2Gamepad(DeviceBase): class Se2Gamepad(DeviceBase):
...@@ -42,10 +54,7 @@ class Se2Gamepad(DeviceBase): ...@@ -42,10 +54,7 @@ class Se2Gamepad(DeviceBase):
def __init__( def __init__(
self, self,
v_x_sensitivity: float = 1.0, cfg: Se2GamepadCfg,
v_y_sensitivity: float = 1.0,
omega_z_sensitivity: float = 1.0,
dead_zone: float = 0.01,
): ):
"""Initialize the gamepad layer. """Initialize the gamepad layer.
...@@ -60,10 +69,11 @@ class Se2Gamepad(DeviceBase): ...@@ -60,10 +69,11 @@ class Se2Gamepad(DeviceBase):
carb_settings_iface = carb.settings.get_settings() carb_settings_iface = carb.settings.get_settings()
carb_settings_iface.set_bool("/persistent/app/omniverse/gamepadCameraControl", False) carb_settings_iface.set_bool("/persistent/app/omniverse/gamepadCameraControl", False)
# store inputs # store inputs
self.v_x_sensitivity = v_x_sensitivity self.v_x_sensitivity = cfg.v_x_sensitivity
self.v_y_sensitivity = v_y_sensitivity self.v_y_sensitivity = cfg.v_y_sensitivity
self.omega_z_sensitivity = omega_z_sensitivity self.omega_z_sensitivity = cfg.omega_z_sensitivity
self.dead_zone = dead_zone self.dead_zone = cfg.dead_zone
self._sim_device = cfg.sim_device
# acquire omniverse interfaces # acquire omniverse interfaces
self._appwindow = omni.appwindow.get_default_app_window() self._appwindow = omni.appwindow.get_default_app_window()
self._input = carb.input.acquire_input_interface() self._input = carb.input.acquire_input_interface()
...@@ -121,13 +131,14 @@ class Se2Gamepad(DeviceBase): ...@@ -121,13 +131,14 @@ class Se2Gamepad(DeviceBase):
""" """
self._additional_callbacks[key] = func self._additional_callbacks[key] = func
def advance(self) -> np.ndarray: def advance(self) -> torch.Tensor:
"""Provides the result from gamepad event state. """Provides the result from gamepad event state.
Returns: Returns:
A 3D array containing the linear (x,y) and angular velocity (z). A tensor containing the linear (x,y) and angular velocity (z).
""" """
return self._resolve_command_buffer(self._base_command_raw) numpy_result = self._resolve_command_buffer(self._base_command_raw)
return torch.tensor(numpy_result, dtype=torch.float32, device=self._sim_device)
""" """
Internal helpers. Internal helpers.
......
...@@ -6,14 +6,26 @@ ...@@ -6,14 +6,26 @@
"""Gamepad controller for SE(3) control.""" """Gamepad controller for SE(3) control."""
import numpy as np import numpy as np
import torch
import weakref import weakref
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass
from scipy.spatial.transform import Rotation from scipy.spatial.transform import Rotation
import carb import carb
import omni import omni
from ..device_base import DeviceBase from ..device_base import DeviceBase, DeviceCfg
@dataclass
class Se3GamepadCfg(DeviceCfg):
"""Configuration for SE3 gamepad devices."""
dead_zone: float = 0.01 # For gamepad devices
pos_sensitivity: float = 1.0
rot_sensitivity: float = 1.6
retargeters: None = None
class Se3Gamepad(DeviceBase): class Se3Gamepad(DeviceBase):
...@@ -47,22 +59,23 @@ class Se3Gamepad(DeviceBase): ...@@ -47,22 +59,23 @@ class Se3Gamepad(DeviceBase):
""" """
def __init__(self, pos_sensitivity: float = 1.0, rot_sensitivity: float = 1.6, dead_zone: float = 0.01): def __init__(
self,
cfg: Se3GamepadCfg,
):
"""Initialize the gamepad layer. """Initialize the gamepad layer.
Args: Args:
pos_sensitivity: Magnitude of input position command scaling. Defaults to 1.0. cfg: Configuration object for gamepad settings.
rot_sensitivity: Magnitude of scale input rotation commands scaling. Defaults to 1.6.
dead_zone: Magnitude of dead zone for gamepad. An event value from the gamepad less than
this value will be ignored. Defaults to 0.01.
""" """
# turn off simulator gamepad control # turn off simulator gamepad control
carb_settings_iface = carb.settings.get_settings() carb_settings_iface = carb.settings.get_settings()
carb_settings_iface.set_bool("/persistent/app/omniverse/gamepadCameraControl", False) carb_settings_iface.set_bool("/persistent/app/omniverse/gamepadCameraControl", False)
# store inputs # store inputs
self.pos_sensitivity = pos_sensitivity self.pos_sensitivity = cfg.pos_sensitivity
self.rot_sensitivity = rot_sensitivity self.rot_sensitivity = cfg.rot_sensitivity
self.dead_zone = dead_zone self.dead_zone = cfg.dead_zone
self._sim_device = cfg.sim_device
# acquire omniverse interfaces # acquire omniverse interfaces
self._appwindow = omni.appwindow.get_default_app_window() self._appwindow = omni.appwindow.get_default_app_window()
self._input = carb.input.acquire_input_interface() self._input = carb.input.acquire_input_interface()
...@@ -127,11 +140,13 @@ class Se3Gamepad(DeviceBase): ...@@ -127,11 +140,13 @@ class Se3Gamepad(DeviceBase):
""" """
self._additional_callbacks[key] = func self._additional_callbacks[key] = func
def advance(self) -> tuple[np.ndarray, bool]: def advance(self) -> torch.Tensor:
"""Provides the result from gamepad event state. """Provides the result from gamepad event state.
Returns: Returns:
A tuple containing the delta pose command and gripper commands. torch.Tensor: A 7-element tensor containing:
- delta pose: First 6 elements as [x, y, z, rx, ry, rz] in meters and radians.
- gripper command: Last element as a binary value (+1.0 for open, -1.0 for close).
""" """
# -- resolve position command # -- resolve position command
delta_pos = self._resolve_command_buffer(self._delta_pose_raw[:, :3]) delta_pos = self._resolve_command_buffer(self._delta_pose_raw[:, :3])
...@@ -140,7 +155,10 @@ class Se3Gamepad(DeviceBase): ...@@ -140,7 +155,10 @@ class Se3Gamepad(DeviceBase):
# -- convert to rotation vector # -- convert to rotation vector
rot_vec = Rotation.from_euler("XYZ", delta_rot).as_rotvec() rot_vec = Rotation.from_euler("XYZ", delta_rot).as_rotvec()
# return the command and gripper state # return the command and gripper state
return np.concatenate([delta_pos, rot_vec]), self._close_gripper gripper_value = -1.0 if self._close_gripper else 1.0
delta_pose = np.concatenate([delta_pos, rot_vec])
command = np.append(delta_pose, gripper_value)
return torch.tensor(command, dtype=torch.float32, device=self._sim_device)
""" """
Internal helpers. Internal helpers.
......
...@@ -5,5 +5,5 @@ ...@@ -5,5 +5,5 @@
"""Keyboard device for SE(2) and SE(3) control.""" """Keyboard device for SE(2) and SE(3) control."""
from .se2_keyboard import Se2Keyboard from .se2_keyboard import Se2Keyboard, Se2KeyboardCfg
from .se3_keyboard import Se3Keyboard from .se3_keyboard import Se3Keyboard, Se3KeyboardCfg
...@@ -6,13 +6,24 @@ ...@@ -6,13 +6,24 @@
"""Keyboard controller for SE(2) control.""" """Keyboard controller for SE(2) control."""
import numpy as np import numpy as np
import torch
import weakref import weakref
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass
import carb import carb
import omni import omni
from ..device_base import DeviceBase from ..device_base import DeviceBase, DeviceCfg
@dataclass
class Se2KeyboardCfg(DeviceCfg):
"""Configuration for SE2 keyboard devices."""
v_x_sensitivity: float = 0.8
v_y_sensitivity: float = 0.4
omega_z_sensitivity: float = 1.0
class Se2Keyboard(DeviceBase): class Se2Keyboard(DeviceBase):
...@@ -39,7 +50,7 @@ class Se2Keyboard(DeviceBase): ...@@ -39,7 +50,7 @@ class Se2Keyboard(DeviceBase):
""" """
def __init__(self, v_x_sensitivity: float = 0.8, v_y_sensitivity: float = 0.4, omega_z_sensitivity: float = 1.0): def __init__(self, cfg: Se2KeyboardCfg):
"""Initialize the keyboard layer. """Initialize the keyboard layer.
Args: Args:
...@@ -48,9 +59,11 @@ class Se2Keyboard(DeviceBase): ...@@ -48,9 +59,11 @@ class Se2Keyboard(DeviceBase):
omega_z_sensitivity: Magnitude of angular velocity along z-direction scaling. Defaults to 1.0. omega_z_sensitivity: Magnitude of angular velocity along z-direction scaling. Defaults to 1.0.
""" """
# store inputs # store inputs
self.v_x_sensitivity = v_x_sensitivity self.v_x_sensitivity = cfg.v_x_sensitivity
self.v_y_sensitivity = v_y_sensitivity self.v_y_sensitivity = cfg.v_y_sensitivity
self.omega_z_sensitivity = omega_z_sensitivity self.omega_z_sensitivity = cfg.omega_z_sensitivity
self._sim_device = cfg.sim_device
# acquire omniverse interfaces # acquire omniverse interfaces
self._appwindow = omni.appwindow.get_default_app_window() self._appwindow = omni.appwindow.get_default_app_window()
self._input = carb.input.acquire_input_interface() self._input = carb.input.acquire_input_interface()
...@@ -107,13 +120,13 @@ class Se2Keyboard(DeviceBase): ...@@ -107,13 +120,13 @@ class Se2Keyboard(DeviceBase):
""" """
self._additional_callbacks[key] = func self._additional_callbacks[key] = func
def advance(self) -> np.ndarray: def advance(self) -> torch.Tensor:
"""Provides the result from keyboard event state. """Provides the result from keyboard event state.
Returns: Returns:
3D array containing the linear (x,y) and angular velocity (z). Tensor containing the linear (x,y) and angular velocity (z).
""" """
return self._base_command return torch.tensor(self._base_command, dtype=torch.float32, device=self._sim_device)
""" """
Internal helpers. Internal helpers.
......
...@@ -6,14 +6,25 @@ ...@@ -6,14 +6,25 @@
"""Keyboard controller for SE(3) control.""" """Keyboard controller for SE(3) control."""
import numpy as np import numpy as np
import torch
import weakref import weakref
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass
from scipy.spatial.transform import Rotation from scipy.spatial.transform import Rotation
import carb import carb
import omni import omni
from ..device_base import DeviceBase from ..device_base import DeviceBase, DeviceCfg
@dataclass
class Se3KeyboardCfg(DeviceCfg):
"""Configuration for SE3 keyboard devices."""
pos_sensitivity: float = 0.4
rot_sensitivity: float = 0.8
retargeters: None = None
class Se3Keyboard(DeviceBase): class Se3Keyboard(DeviceBase):
...@@ -47,16 +58,16 @@ class Se3Keyboard(DeviceBase): ...@@ -47,16 +58,16 @@ class Se3Keyboard(DeviceBase):
""" """
def __init__(self, pos_sensitivity: float = 0.4, rot_sensitivity: float = 0.8): def __init__(self, cfg: Se3KeyboardCfg):
"""Initialize the keyboard layer. """Initialize the keyboard layer.
Args: Args:
pos_sensitivity: Magnitude of input position command scaling. Defaults to 0.05. cfg: Configuration object for keyboard settings.
rot_sensitivity: Magnitude of scale input rotation commands scaling. Defaults to 0.5.
""" """
# store inputs # store inputs
self.pos_sensitivity = pos_sensitivity self.pos_sensitivity = cfg.pos_sensitivity
self.rot_sensitivity = rot_sensitivity self.rot_sensitivity = cfg.rot_sensitivity
self._sim_device = cfg.sim_device
# acquire omniverse interfaces # acquire omniverse interfaces
self._appwindow = omni.appwindow.get_default_app_window() self._appwindow = omni.appwindow.get_default_app_window()
self._input = carb.input.acquire_input_interface() self._input = carb.input.acquire_input_interface()
...@@ -117,16 +128,21 @@ class Se3Keyboard(DeviceBase): ...@@ -117,16 +128,21 @@ class Se3Keyboard(DeviceBase):
""" """
self._additional_callbacks[key] = func self._additional_callbacks[key] = func
def advance(self) -> tuple[np.ndarray, bool]: def advance(self) -> torch.Tensor:
"""Provides the result from keyboard event state. """Provides the result from keyboard event state.
Returns: Returns:
A tuple containing the delta pose command and gripper commands. torch.Tensor: A 7-element tensor containing:
- delta pose: First 6 elements as [x, y, z, rx, ry, rz] in meters and radians.
- gripper command: Last element as a binary value (+1.0 for open, -1.0 for close).
""" """
# convert to rotation vector # convert to rotation vector
rot_vec = Rotation.from_euler("XYZ", self._delta_rot).as_rotvec() rot_vec = Rotation.from_euler("XYZ", self._delta_rot).as_rotvec()
# return the command and gripper state # return the command and gripper state
return np.concatenate([self._delta_pos, rot_vec]), self._close_gripper gripper_value = -1.0 if self._close_gripper else 1.0
delta_pose = np.concatenate([self._delta_pos, rot_vec])
command = np.append(delta_pose, gripper_value)
return torch.tensor(command, dtype=torch.float32, device=self._sim_device)
""" """
Internal helpers. Internal helpers.
......
...@@ -5,5 +5,5 @@ ...@@ -5,5 +5,5 @@
"""Keyboard device for SE(2) and SE(3) control.""" """Keyboard device for SE(2) and SE(3) control."""
from .openxr_device import OpenXRDevice from .openxr_device import OpenXRDevice, OpenXRDeviceCfg
from .xr_cfg import XrCfg from .xr_cfg import XrCfg
...@@ -8,6 +8,7 @@ ...@@ -8,6 +8,7 @@
import contextlib import contextlib
import numpy as np import numpy as np
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass
from enum import Enum from enum import Enum
from typing import Any from typing import Any
...@@ -16,24 +17,37 @@ import carb ...@@ -16,24 +17,37 @@ import carb
from isaaclab.devices.openxr.common import HAND_JOINT_NAMES from isaaclab.devices.openxr.common import HAND_JOINT_NAMES
from isaaclab.devices.retargeter_base import RetargeterBase from isaaclab.devices.retargeter_base import RetargeterBase
from ..device_base import DeviceBase from ..device_base import DeviceBase, DeviceCfg
from .xr_cfg import XrCfg from .xr_cfg import XrCfg
# For testing purposes, we need to mock the XRCore, XRPoseValidityFlags classes
XRCore = None
XRPoseValidityFlags = None
with contextlib.suppress(ModuleNotFoundError): with contextlib.suppress(ModuleNotFoundError):
from omni.kit.xr.core import XRCore, XRPoseValidityFlags from omni.kit.xr.core import XRCore, XRPoseValidityFlags
from isaacsim.core.prims import SingleXFormPrim from isaacsim.core.prims import SingleXFormPrim
@dataclass
class OpenXRDeviceCfg(DeviceCfg):
"""Configuration for OpenXR devices."""
xr_cfg: XrCfg | None = None
class OpenXRDevice(DeviceBase): class OpenXRDevice(DeviceBase):
"""An OpenXR-powered device for teleoperation and interaction. """An OpenXR-powered device for teleoperation and interaction.
This device tracks hand joints using OpenXR and makes them available as: This device tracks hand joints using OpenXR and makes them available as:
1. A dictionary of joint poses (when used directly) 1. A dictionary of tracking data (when used without retargeters)
2. Retargeted commands for robot control (when a retargeter is provided) 2. Retargeted commands for robot control (when retargeters are provided)
Data format: Raw data format (_get_raw_data output):
* Joint poses: Each pose is a 7D vector (x, y, z, qw, qx, qy, qz) in meters and quaternion units * A dictionary with keys matching TrackingTarget enum values (HAND_LEFT, HAND_RIGHT, HEAD)
* Dictionary keys: Joint names from HAND_JOINT_NAMES in isaaclab.devices.openxr.common * Each hand tracking entry contains a dictionary of joint poses
* Each joint pose is a 7D vector (x, y, z, qw, qx, qy, qz) in meters and quaternion units
* Joint names are defined in HAND_JOINT_NAMES from isaaclab.devices.openxr.common
* Supported joints include palm, wrist, and joints for thumb, index, middle, ring and little fingers * Supported joints include palm, wrist, and joints for thumb, index, middle, ring and little fingers
Teleop commands: Teleop commands:
...@@ -42,7 +56,7 @@ class OpenXRDevice(DeviceBase): ...@@ -42,7 +56,7 @@ class OpenXRDevice(DeviceBase):
* "STOP": Pause hand tracking data flow * "STOP": Pause hand tracking data flow
* "RESET": Reset the tracking and signal simulation reset * "RESET": Reset the tracking and signal simulation reset
The device can track the left hand, right hand, head position, or any combination of these The device tracks the left hand, right hand, head position, or any combination of these
based on the TrackingTarget enum values. When retargeters are provided, the raw tracking based on the TrackingTarget enum values. When retargeters are provided, the raw tracking
data is transformed into robot control commands suitable for teleoperation. data is transformed into robot control commands suitable for teleoperation.
""" """
...@@ -64,18 +78,17 @@ class OpenXRDevice(DeviceBase): ...@@ -64,18 +78,17 @@ class OpenXRDevice(DeviceBase):
def __init__( def __init__(
self, self,
xr_cfg: XrCfg | None, cfg: OpenXRDeviceCfg,
retargeters: list[RetargeterBase] | None = None, retargeters: list[RetargeterBase] | None = None,
): ):
"""Initialize the OpenXR device. """Initialize the OpenXR device.
Args: Args:
xr_cfg: Configuration object for OpenXR settings. If None, default settings are used. cfg: Configuration object for OpenXR settings.
retargeters: List of retargeters to transform tracking data into robot commands. retargeters: List of retargeter instances to use for transforming raw tracking data.
If None or empty list, raw tracking data will be returned.
""" """
super().__init__(retargeters) super().__init__(retargeters)
self._xr_cfg = xr_cfg or XrCfg() self._xr_cfg = cfg.xr_cfg or XrCfg()
self._additional_callbacks = dict() self._additional_callbacks = dict()
self._vc_subscription = ( self._vc_subscription = (
XRCore.get_singleton() XRCore.get_singleton()
...@@ -91,7 +104,6 @@ class OpenXRDevice(DeviceBase): ...@@ -91,7 +104,6 @@ class OpenXRDevice(DeviceBase):
self._previous_joint_poses_right = {name: default_pose.copy() for name in HAND_JOINT_NAMES} self._previous_joint_poses_right = {name: default_pose.copy() for name in HAND_JOINT_NAMES}
self._previous_headpose = default_pose.copy() self._previous_headpose = default_pose.copy()
# Specify the placement of the simulation when viewed in an XR device using a prim.
xr_anchor = SingleXFormPrim("/XRAnchor", position=self._xr_cfg.anchor_pos, orientation=self._xr_cfg.anchor_rot) xr_anchor = SingleXFormPrim("/XRAnchor", position=self._xr_cfg.anchor_pos, orientation=self._xr_cfg.anchor_rot)
carb.settings.get_settings().set_float("/persistent/xr/profile/ar/render/nearPlane", self._xr_cfg.near_plane) carb.settings.get_settings().set_float("/persistent/xr/profile/ar/render/nearPlane", self._xr_cfg.near_plane)
carb.settings.get_settings().set_string("/persistent/xr/profile/ar/anchorMode", "custom anchor") carb.settings.get_settings().set_string("/persistent/xr/profile/ar/anchorMode", "custom anchor")
...@@ -176,10 +188,10 @@ class OpenXRDevice(DeviceBase): ...@@ -176,10 +188,10 @@ class OpenXRDevice(DeviceBase):
"""Get the latest tracking data from the OpenXR runtime. """Get the latest tracking data from the OpenXR runtime.
Returns: Returns:
Dictionary containing tracking data for: Dictionary with TrackingTarget enum keys (HAND_LEFT, HAND_RIGHT, HEAD) containing:
- Left hand joint poses (26 joints with position and orientation) - Left hand joint poses: Dictionary of 26 joints with position and orientation
- Right hand joint poses (26 joints with position and orientation) - Right hand joint poses: Dictionary of 26 joints with position and orientation
- Head pose (position and orientation) - Head pose: Single 7-element array with position and orientation
Each pose is represented as a 7-element array: [x, y, z, qw, qx, qy, qz] Each pose is represented as a 7-element array: [x, y, z, qw, qx, qy, qz]
where the first 3 elements are position and the last 4 are quaternion orientation. where the first 3 elements are position and the last 4 are quaternion orientation.
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
# SPDX-License-Identifier: BSD-3-Clause # SPDX-License-Identifier: BSD-3-Clause
"""Retargeters for mapping input device data to robot commands.""" """Retargeters for mapping input device data to robot commands."""
from .humanoid.fourier.gr1t2_retargeter import GR1T2Retargeter from .humanoid.fourier.gr1t2_retargeter import GR1T2Retargeter, GR1T2RetargeterCfg
from .manipulator.gripper_retargeter import GripperRetargeter from .manipulator.gripper_retargeter import GripperRetargeter, GripperRetargeterCfg
from .manipulator.se3_abs_retargeter import Se3AbsRetargeter from .manipulator.se3_abs_retargeter import Se3AbsRetargeter, Se3AbsRetargeterCfg
from .manipulator.se3_rel_retargeter import Se3RelRetargeter from .manipulator.se3_rel_retargeter import Se3RelRetargeter, Se3RelRetargeterCfg
...@@ -11,11 +11,12 @@ ...@@ -11,11 +11,12 @@
import contextlib import contextlib
import numpy as np import numpy as np
import torch import torch
from dataclasses import dataclass
import isaaclab.sim as sim_utils import isaaclab.sim as sim_utils
import isaaclab.utils.math as PoseUtils import isaaclab.utils.math as PoseUtils
from isaaclab.devices import OpenXRDevice from isaaclab.devices import OpenXRDevice
from isaaclab.devices.retargeter_base import RetargeterBase from isaaclab.devices.retargeter_base import RetargeterBase, RetargeterCfg
from isaaclab.markers import VisualizationMarkers, VisualizationMarkersCfg from isaaclab.markers import VisualizationMarkers, VisualizationMarkersCfg
# This import exception is suppressed because gr1_t2_dex_retargeting_utils depends on pinocchio which is not available on windows # This import exception is suppressed because gr1_t2_dex_retargeting_utils depends on pinocchio which is not available on windows
...@@ -23,6 +24,15 @@ with contextlib.suppress(Exception): ...@@ -23,6 +24,15 @@ with contextlib.suppress(Exception):
from .gr1_t2_dex_retargeting_utils import GR1TR2DexRetargeting from .gr1_t2_dex_retargeting_utils import GR1TR2DexRetargeting
@dataclass
class GR1T2RetargeterCfg(RetargeterCfg):
"""Configuration for the GR1T2 retargeter."""
enable_visualization: bool = False
num_open_xr_hand_joints: int = 100
hand_joint_names: list[str] | None = None # List of robot hand joint names
class GR1T2Retargeter(RetargeterBase): class GR1T2Retargeter(RetargeterBase):
"""Retargets OpenXR hand tracking data to GR1T2 hand end-effector commands. """Retargets OpenXR hand tracking data to GR1T2 hand end-effector commands.
...@@ -32,10 +42,7 @@ class GR1T2Retargeter(RetargeterBase): ...@@ -32,10 +42,7 @@ class GR1T2Retargeter(RetargeterBase):
def __init__( def __init__(
self, self,
enable_visualization: bool = False, cfg: GR1T2RetargeterCfg,
num_open_xr_hand_joints: int = 100,
device: torch.device = torch.device("cuda:0"),
hand_joint_names: list[str] = [],
): ):
"""Initialize the GR1T2 hand retargeter. """Initialize the GR1T2 hand retargeter.
...@@ -46,13 +53,13 @@ class GR1T2Retargeter(RetargeterBase): ...@@ -46,13 +53,13 @@ class GR1T2Retargeter(RetargeterBase):
hand_joint_names: List of robot hand joint names hand_joint_names: List of robot hand joint names
""" """
self._hand_joint_names = hand_joint_names self._hand_joint_names = cfg.hand_joint_names
self._hands_controller = GR1TR2DexRetargeting(self._hand_joint_names) self._hands_controller = GR1TR2DexRetargeting(self._hand_joint_names)
# Initialize visualization if enabled # Initialize visualization if enabled
self._enable_visualization = enable_visualization self._enable_visualization = cfg.enable_visualization
self._num_open_xr_hand_joints = num_open_xr_hand_joints self._num_open_xr_hand_joints = cfg.num_open_xr_hand_joints
self._device = device self._sim_device = cfg.sim_device
if self._enable_visualization: if self._enable_visualization:
marker_cfg = VisualizationMarkersCfg( marker_cfg = VisualizationMarkersCfg(
prim_path="/Visuals/markers", prim_path="/Visuals/markers",
...@@ -65,7 +72,7 @@ class GR1T2Retargeter(RetargeterBase): ...@@ -65,7 +72,7 @@ class GR1T2Retargeter(RetargeterBase):
) )
self._markers = VisualizationMarkers(marker_cfg) self._markers = VisualizationMarkers(marker_cfg)
def retarget(self, data: dict) -> tuple[np.ndarray, np.ndarray, np.ndarray]: def retarget(self, data: dict) -> torch.Tensor:
"""Convert hand joint poses to robot end-effector commands. """Convert hand joint poses to robot end-effector commands.
Args: Args:
...@@ -91,7 +98,7 @@ class GR1T2Retargeter(RetargeterBase): ...@@ -91,7 +98,7 @@ class GR1T2Retargeter(RetargeterBase):
joints_position[::2] = np.array([pose[:3] for pose in left_hand_poses.values()]) joints_position[::2] = np.array([pose[:3] for pose in left_hand_poses.values()])
joints_position[1::2] = np.array([pose[:3] for pose in right_hand_poses.values()]) joints_position[1::2] = np.array([pose[:3] for pose in right_hand_poses.values()])
self._markers.visualize(translations=torch.tensor(joints_position, device=self._device)) self._markers.visualize(translations=torch.tensor(joints_position, device=self._sim_device))
# Create array of zeros with length matching number of joint names # Create array of zeros with length matching number of joint names
left_hands_pos = self._hands_controller.compute_left(left_hand_poses) left_hands_pos = self._hands_controller.compute_left(left_hand_poses)
...@@ -107,7 +114,13 @@ class GR1T2Retargeter(RetargeterBase): ...@@ -107,7 +114,13 @@ class GR1T2Retargeter(RetargeterBase):
right_hand_joints = right_retargeted_hand_joints right_hand_joints = right_retargeted_hand_joints
retargeted_hand_joints = left_hand_joints + right_hand_joints retargeted_hand_joints = left_hand_joints + right_hand_joints
return left_wrist, self._retarget_abs(right_wrist), retargeted_hand_joints # Convert numpy arrays to tensors and concatenate them
left_wrist_tensor = torch.tensor(left_wrist, dtype=torch.float32, device=self._sim_device)
right_wrist_tensor = torch.tensor(self._retarget_abs(right_wrist), dtype=torch.float32, device=self._sim_device)
hand_joints_tensor = torch.tensor(retargeted_hand_joints, dtype=torch.float32, device=self._sim_device)
# Combine all tensors into a single tensor
return torch.cat([left_wrist_tensor, right_wrist_tensor, hand_joints_tensor])
def _retarget_abs(self, wrist: np.ndarray) -> np.ndarray: def _retarget_abs(self, wrist: np.ndarray) -> np.ndarray:
"""Handle absolute pose retargeting. """Handle absolute pose retargeting.
......
...@@ -8,6 +8,6 @@ ...@@ -8,6 +8,6 @@
This module provides functionality for retargeting motion to Franka robots. This module provides functionality for retargeting motion to Franka robots.
""" """
from .gripper_retargeter import GripperRetargeter from .gripper_retargeter import GripperRetargeter, GripperRetargeterCfg
from .se3_abs_retargeter import Se3AbsRetargeter from .se3_abs_retargeter import Se3AbsRetargeter, Se3AbsRetargeterCfg
from .se3_rel_retargeter import Se3RelRetargeter from .se3_rel_retargeter import Se3RelRetargeter, Se3RelRetargeterCfg
...@@ -3,10 +3,19 @@ ...@@ -3,10 +3,19 @@
# #
# SPDX-License-Identifier: BSD-3-Clause # SPDX-License-Identifier: BSD-3-Clause
import numpy as np import numpy as np
import torch
from dataclasses import dataclass
from typing import Final from typing import Final
from isaaclab.devices import OpenXRDevice from isaaclab.devices import OpenXRDevice
from isaaclab.devices.retargeter_base import RetargeterBase from isaaclab.devices.retargeter_base import RetargeterBase, RetargeterCfg
@dataclass
class GripperRetargeterCfg(RetargeterCfg):
"""Configuration for gripper retargeter."""
bound_hand: OpenXRDevice.TrackingTarget = OpenXRDevice.TrackingTarget.HAND_RIGHT
class GripperRetargeter(RetargeterBase): class GripperRetargeter(RetargeterBase):
...@@ -27,20 +36,21 @@ class GripperRetargeter(RetargeterBase): ...@@ -27,20 +36,21 @@ class GripperRetargeter(RetargeterBase):
def __init__( def __init__(
self, self,
bound_hand: OpenXRDevice.TrackingTarget, cfg: GripperRetargeterCfg,
): ):
super().__init__(cfg)
"""Initialize the gripper retargeter.""" """Initialize the gripper retargeter."""
# Store the hand to track # Store the hand to track
if bound_hand not in [OpenXRDevice.TrackingTarget.HAND_LEFT, OpenXRDevice.TrackingTarget.HAND_RIGHT]: if cfg.bound_hand not in [OpenXRDevice.TrackingTarget.HAND_LEFT, OpenXRDevice.TrackingTarget.HAND_RIGHT]:
raise ValueError( raise ValueError(
"bound_hand must be either OpenXRDevice.TrackingTarget.HAND_LEFT or" "bound_hand must be either OpenXRDevice.TrackingTarget.HAND_LEFT or"
" OpenXRDevice.TrackingTarget.HAND_RIGHT" " OpenXRDevice.TrackingTarget.HAND_RIGHT"
) )
self.bound_hand = bound_hand self.bound_hand = cfg.bound_hand
# Initialize gripper state # Initialize gripper state
self._previous_gripper_command = False self._previous_gripper_command = False
def retarget(self, data: dict) -> bool: def retarget(self, data: dict) -> torch.Tensor:
"""Convert hand joint poses to gripper command. """Convert hand joint poses to gripper command.
Args: Args:
...@@ -48,7 +58,7 @@ class GripperRetargeter(RetargeterBase): ...@@ -48,7 +58,7 @@ class GripperRetargeter(RetargeterBase):
The joint names are defined in isaaclab.devices.openxr.common.HAND_JOINT_NAMES The joint names are defined in isaaclab.devices.openxr.common.HAND_JOINT_NAMES
Returns: Returns:
bool: Gripper command where True = close gripper, False = open gripper torch.Tensor: Tensor containing a single bool value where True = close gripper, False = open gripper
""" """
# Extract key joint poses # Extract key joint poses
hand_data = data[self.bound_hand] hand_data = data[self.bound_hand]
...@@ -56,8 +66,10 @@ class GripperRetargeter(RetargeterBase): ...@@ -56,8 +66,10 @@ class GripperRetargeter(RetargeterBase):
index_tip = hand_data["index_tip"] index_tip = hand_data["index_tip"]
# Calculate gripper command with hysteresis # Calculate gripper command with hysteresis
gripper_command = self._calculate_gripper_command(thumb_tip[:3], index_tip[:3]) gripper_command_bool = self._calculate_gripper_command(thumb_tip[:3], index_tip[:3])
return gripper_command gripper_value = -1.0 if gripper_command_bool else 1.0
return torch.tensor([gripper_value], dtype=torch.float32, device=self._sim_device)
def _calculate_gripper_command(self, thumb_pos: np.ndarray, index_pos: np.ndarray) -> bool: def _calculate_gripper_command(self, thumb_pos: np.ndarray, index_pos: np.ndarray) -> bool:
"""Calculate gripper command from finger positions with hysteresis. """Calculate gripper command from finger positions with hysteresis.
......
...@@ -3,14 +3,27 @@ ...@@ -3,14 +3,27 @@
# #
# SPDX-License-Identifier: BSD-3-Clause # SPDX-License-Identifier: BSD-3-Clause
import numpy as np import numpy as np
import torch
from dataclasses import dataclass
from scipy.spatial.transform import Rotation, Slerp from scipy.spatial.transform import Rotation, Slerp
from isaaclab.devices import OpenXRDevice from isaaclab.devices import OpenXRDevice
from isaaclab.devices.retargeter_base import RetargeterBase from isaaclab.devices.retargeter_base import RetargeterBase, RetargeterCfg
from isaaclab.markers import VisualizationMarkers from isaaclab.markers import VisualizationMarkers
from isaaclab.markers.config import FRAME_MARKER_CFG from isaaclab.markers.config import FRAME_MARKER_CFG
@dataclass
class Se3AbsRetargeterCfg(RetargeterCfg):
"""Configuration for absolute position retargeter."""
zero_out_xy_rotation: bool = True
use_wrist_rotation: bool = False
use_wrist_position: bool = True
enable_visualization: bool = False
bound_hand: OpenXRDevice.TrackingTarget = OpenXRDevice.TrackingTarget.HAND_RIGHT
class Se3AbsRetargeter(RetargeterBase): class Se3AbsRetargeter(RetargeterBase):
"""Retargets OpenXR hand tracking data to end-effector commands using absolute positioning. """Retargets OpenXR hand tracking data to end-effector commands using absolute positioning.
...@@ -26,11 +39,7 @@ class Se3AbsRetargeter(RetargeterBase): ...@@ -26,11 +39,7 @@ class Se3AbsRetargeter(RetargeterBase):
def __init__( def __init__(
self, self,
bound_hand: OpenXRDevice.TrackingTarget, cfg: Se3AbsRetargeterCfg,
zero_out_xy_rotation: bool = False,
use_wrist_rotation: bool = False,
use_wrist_position: bool = False,
enable_visualization: bool = False,
): ):
"""Initialize the retargeter. """Initialize the retargeter.
...@@ -40,21 +49,23 @@ class Se3AbsRetargeter(RetargeterBase): ...@@ -40,21 +49,23 @@ class Se3AbsRetargeter(RetargeterBase):
use_wrist_rotation: If True, use wrist rotation instead of finger average use_wrist_rotation: If True, use wrist rotation instead of finger average
use_wrist_position: If True, use wrist position instead of pinch position use_wrist_position: If True, use wrist position instead of pinch position
enable_visualization: If True, visualize the target pose in the scene enable_visualization: If True, visualize the target pose in the scene
device: The device to place the returned tensor on ('cpu' or 'cuda')
""" """
if bound_hand not in [OpenXRDevice.TrackingTarget.HAND_LEFT, OpenXRDevice.TrackingTarget.HAND_RIGHT]: super().__init__(cfg)
if cfg.bound_hand not in [OpenXRDevice.TrackingTarget.HAND_LEFT, OpenXRDevice.TrackingTarget.HAND_RIGHT]:
raise ValueError( raise ValueError(
"bound_hand must be either OpenXRDevice.TrackingTarget.HAND_LEFT or" "bound_hand must be either OpenXRDevice.TrackingTarget.HAND_LEFT or"
" OpenXRDevice.TrackingTarget.HAND_RIGHT" " OpenXRDevice.TrackingTarget.HAND_RIGHT"
) )
self.bound_hand = bound_hand self.bound_hand = cfg.bound_hand
self._zero_out_xy_rotation = zero_out_xy_rotation self._zero_out_xy_rotation = cfg.zero_out_xy_rotation
self._use_wrist_rotation = use_wrist_rotation self._use_wrist_rotation = cfg.use_wrist_rotation
self._use_wrist_position = use_wrist_position self._use_wrist_position = cfg.use_wrist_position
# Initialize visualization if enabled # Initialize visualization if enabled
self._enable_visualization = enable_visualization self._enable_visualization = cfg.enable_visualization
if enable_visualization: if cfg.enable_visualization:
frame_marker_cfg = FRAME_MARKER_CFG.copy() frame_marker_cfg = FRAME_MARKER_CFG.copy()
frame_marker_cfg.markers["frame"].scale = (0.1, 0.1, 0.1) frame_marker_cfg.markers["frame"].scale = (0.1, 0.1, 0.1)
self._goal_marker = VisualizationMarkers(frame_marker_cfg.replace(prim_path="/Visuals/ee_goal")) self._goal_marker = VisualizationMarkers(frame_marker_cfg.replace(prim_path="/Visuals/ee_goal"))
...@@ -62,7 +73,7 @@ class Se3AbsRetargeter(RetargeterBase): ...@@ -62,7 +73,7 @@ class Se3AbsRetargeter(RetargeterBase):
self._visualization_pos = np.zeros(3) self._visualization_pos = np.zeros(3)
self._visualization_rot = np.array([1.0, 0.0, 0.0, 0.0]) self._visualization_rot = np.array([1.0, 0.0, 0.0, 0.0])
def retarget(self, data: dict) -> np.ndarray: def retarget(self, data: dict) -> torch.Tensor:
"""Convert hand joint poses to robot end-effector command. """Convert hand joint poses to robot end-effector command.
Args: Args:
...@@ -70,7 +81,7 @@ class Se3AbsRetargeter(RetargeterBase): ...@@ -70,7 +81,7 @@ class Se3AbsRetargeter(RetargeterBase):
The joint names are defined in isaaclab.devices.openxr.common.HAND_JOINT_NAMES The joint names are defined in isaaclab.devices.openxr.common.HAND_JOINT_NAMES
Returns: Returns:
np.ndarray: 7D array containing position (xyz) and orientation (quaternion) torch.Tensor: 7D tensor containing position (xyz) and orientation (quaternion)
for the robot end-effector for the robot end-effector
""" """
# Extract key joint poses from the bound hand # Extract key joint poses from the bound hand
...@@ -79,7 +90,10 @@ class Se3AbsRetargeter(RetargeterBase): ...@@ -79,7 +90,10 @@ class Se3AbsRetargeter(RetargeterBase):
index_tip = hand_data.get("index_tip") index_tip = hand_data.get("index_tip")
wrist = hand_data.get("wrist") wrist = hand_data.get("wrist")
ee_command = self._retarget_abs(thumb_tip, index_tip, wrist) ee_command_np = self._retarget_abs(thumb_tip, index_tip, wrist)
# Convert to torch tensor
ee_command = torch.tensor(ee_command_np, dtype=torch.float32, device=self._sim_device)
return ee_command return ee_command
......
...@@ -3,14 +3,31 @@ ...@@ -3,14 +3,31 @@
# #
# SPDX-License-Identifier: BSD-3-Clause # SPDX-License-Identifier: BSD-3-Clause
import numpy as np import numpy as np
import torch
from dataclasses import dataclass
from scipy.spatial.transform import Rotation from scipy.spatial.transform import Rotation
from isaaclab.devices import OpenXRDevice from isaaclab.devices import OpenXRDevice
from isaaclab.devices.retargeter_base import RetargeterBase from isaaclab.devices.retargeter_base import RetargeterBase, RetargeterCfg
from isaaclab.markers import VisualizationMarkers from isaaclab.markers import VisualizationMarkers
from isaaclab.markers.config import FRAME_MARKER_CFG from isaaclab.markers.config import FRAME_MARKER_CFG
@dataclass
class Se3RelRetargeterCfg(RetargeterCfg):
"""Configuration for relative position retargeter."""
zero_out_xy_rotation: bool = True
use_wrist_rotation: bool = False
use_wrist_position: bool = True
delta_pos_scale_factor: float = 10.0
delta_rot_scale_factor: float = 10.0
alpha_pos: float = 0.5
alpha_rot: float = 0.5
enable_visualization: bool = False
bound_hand: OpenXRDevice.TrackingTarget = OpenXRDevice.TrackingTarget.HAND_RIGHT
class Se3RelRetargeter(RetargeterBase): class Se3RelRetargeter(RetargeterBase):
"""Retargets OpenXR hand tracking data to end-effector commands using relative positioning. """Retargets OpenXR hand tracking data to end-effector commands using relative positioning.
...@@ -27,15 +44,7 @@ class Se3RelRetargeter(RetargeterBase): ...@@ -27,15 +44,7 @@ class Se3RelRetargeter(RetargeterBase):
def __init__( def __init__(
self, self,
bound_hand: OpenXRDevice.TrackingTarget, cfg: Se3RelRetargeterCfg,
zero_out_xy_rotation: bool = False,
use_wrist_rotation: bool = False,
use_wrist_position: bool = True,
delta_pos_scale_factor: float = 10.0,
delta_rot_scale_factor: float = 10.0,
alpha_pos: float = 0.5,
alpha_rot: float = 0.5,
enable_visualization: bool = False,
): ):
"""Initialize the relative motion retargeter. """Initialize the relative motion retargeter.
...@@ -49,22 +58,24 @@ class Se3RelRetargeter(RetargeterBase): ...@@ -49,22 +58,24 @@ class Se3RelRetargeter(RetargeterBase):
alpha_pos: Position smoothing parameter (0-1); higher values track more closely to input, lower values smooth more alpha_pos: Position smoothing parameter (0-1); higher values track more closely to input, lower values smooth more
alpha_rot: Rotation smoothing parameter (0-1); higher values track more closely to input, lower values smooth more alpha_rot: Rotation smoothing parameter (0-1); higher values track more closely to input, lower values smooth more
enable_visualization: If True, show a visual marker representing the target end-effector pose enable_visualization: If True, show a visual marker representing the target end-effector pose
device: The device to place the returned tensor on ('cpu' or 'cuda')
""" """
# Store the hand to track # Store the hand to track
if bound_hand not in [OpenXRDevice.TrackingTarget.HAND_LEFT, OpenXRDevice.TrackingTarget.HAND_RIGHT]: if cfg.bound_hand not in [OpenXRDevice.TrackingTarget.HAND_LEFT, OpenXRDevice.TrackingTarget.HAND_RIGHT]:
raise ValueError( raise ValueError(
"bound_hand must be either OpenXRDevice.TrackingTarget.HAND_LEFT or" "bound_hand must be either OpenXRDevice.TrackingTarget.HAND_LEFT or"
" OpenXRDevice.TrackingTarget.HAND_RIGHT" " OpenXRDevice.TrackingTarget.HAND_RIGHT"
) )
self.bound_hand = bound_hand super().__init__(cfg)
self.bound_hand = cfg.bound_hand
self._zero_out_xy_rotation = zero_out_xy_rotation self._zero_out_xy_rotation = cfg.zero_out_xy_rotation
self._use_wrist_rotation = use_wrist_rotation self._use_wrist_rotation = cfg.use_wrist_rotation
self._use_wrist_position = use_wrist_position self._use_wrist_position = cfg.use_wrist_position
self._delta_pos_scale_factor = delta_pos_scale_factor self._delta_pos_scale_factor = cfg.delta_pos_scale_factor
self._delta_rot_scale_factor = delta_rot_scale_factor self._delta_rot_scale_factor = cfg.delta_rot_scale_factor
self._alpha_pos = alpha_pos self._alpha_pos = cfg.alpha_pos
self._alpha_rot = alpha_rot self._alpha_rot = cfg.alpha_rot
# Initialize smoothing state # Initialize smoothing state
self._smoothed_delta_pos = np.zeros(3) self._smoothed_delta_pos = np.zeros(3)
...@@ -75,8 +86,8 @@ class Se3RelRetargeter(RetargeterBase): ...@@ -75,8 +86,8 @@ class Se3RelRetargeter(RetargeterBase):
self._rotation_threshold = 0.01 self._rotation_threshold = 0.01
# Initialize visualization if enabled # Initialize visualization if enabled
self._enable_visualization = enable_visualization self._enable_visualization = cfg.enable_visualization
if enable_visualization: if cfg.enable_visualization:
frame_marker_cfg = FRAME_MARKER_CFG.copy() frame_marker_cfg = FRAME_MARKER_CFG.copy()
frame_marker_cfg.markers["frame"].scale = (0.1, 0.1, 0.1) frame_marker_cfg.markers["frame"].scale = (0.1, 0.1, 0.1)
self._goal_marker = VisualizationMarkers(frame_marker_cfg.replace(prim_path="/Visuals/ee_goal")) self._goal_marker = VisualizationMarkers(frame_marker_cfg.replace(prim_path="/Visuals/ee_goal"))
...@@ -88,7 +99,7 @@ class Se3RelRetargeter(RetargeterBase): ...@@ -88,7 +99,7 @@ class Se3RelRetargeter(RetargeterBase):
self._previous_index_tip = np.array([0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0], dtype=np.float32) self._previous_index_tip = np.array([0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0], dtype=np.float32)
self._previous_wrist = np.array([0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0], dtype=np.float32) self._previous_wrist = np.array([0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0], dtype=np.float32)
def retarget(self, data: dict) -> np.ndarray: def retarget(self, data: dict) -> torch.Tensor:
"""Convert hand joint poses to robot end-effector command. """Convert hand joint poses to robot end-effector command.
Args: Args:
...@@ -96,7 +107,7 @@ class Se3RelRetargeter(RetargeterBase): ...@@ -96,7 +107,7 @@ class Se3RelRetargeter(RetargeterBase):
The joint names are defined in isaaclab.devices.openxr.common.HAND_JOINT_NAMES The joint names are defined in isaaclab.devices.openxr.common.HAND_JOINT_NAMES
Returns: Returns:
np.ndarray: 6D array containing position (xyz) and rotation vector (rx,ry,rz) torch.Tensor: 6D tensor containing position (xyz) and rotation vector (rx,ry,rz)
for the robot end-effector for the robot end-effector
""" """
# Extract key joint poses from the bound hand # Extract key joint poses from the bound hand
...@@ -108,12 +119,15 @@ class Se3RelRetargeter(RetargeterBase): ...@@ -108,12 +119,15 @@ class Se3RelRetargeter(RetargeterBase):
delta_thumb_tip = self._calculate_delta_pose(thumb_tip, self._previous_thumb_tip) delta_thumb_tip = self._calculate_delta_pose(thumb_tip, self._previous_thumb_tip)
delta_index_tip = self._calculate_delta_pose(index_tip, self._previous_index_tip) delta_index_tip = self._calculate_delta_pose(index_tip, self._previous_index_tip)
delta_wrist = self._calculate_delta_pose(wrist, self._previous_wrist) delta_wrist = self._calculate_delta_pose(wrist, self._previous_wrist)
ee_command = self._retarget_rel(delta_thumb_tip, delta_index_tip, delta_wrist) ee_command_np = self._retarget_rel(delta_thumb_tip, delta_index_tip, delta_wrist)
self._previous_thumb_tip = thumb_tip.copy() self._previous_thumb_tip = thumb_tip.copy()
self._previous_index_tip = index_tip.copy() self._previous_index_tip = index_tip.copy()
self._previous_wrist = wrist.copy() self._previous_wrist = wrist.copy()
# Convert to torch tensor
ee_command = torch.tensor(ee_command_np, dtype=torch.float32, device=self._sim_device)
return ee_command return ee_command
def _calculate_delta_pose(self, joint_pose: np.ndarray, previous_joint_pose: np.ndarray) -> np.ndarray: def _calculate_delta_pose(self, joint_pose: np.ndarray, previous_joint_pose: np.ndarray) -> np.ndarray:
......
...@@ -4,9 +4,17 @@ ...@@ -4,9 +4,17 @@
# SPDX-License-Identifier: BSD-3-Clause # SPDX-License-Identifier: BSD-3-Clause
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any from typing import Any
@dataclass
class RetargeterCfg:
"""Base configuration for hand tracking retargeters."""
sim_device: str = "cpu"
class RetargeterBase(ABC): class RetargeterBase(ABC):
"""Base interface for input data retargeting. """Base interface for input data retargeting.
...@@ -18,6 +26,14 @@ class RetargeterBase(ABC): ...@@ -18,6 +26,14 @@ class RetargeterBase(ABC):
- Sensor data to control signals - Sensor data to control signals
""" """
def __init__(self, cfg: RetargeterCfg):
"""Initialize the retargeter.
Args:
cfg: Configuration for the retargeter
"""
self._sim_device = cfg.sim_device
@abstractmethod @abstractmethod
def retarget(self, data: Any) -> Any: def retarget(self, data: Any) -> Any:
"""Retarget input data to desired output format. """Retarget input data to desired output format.
......
...@@ -5,5 +5,5 @@ ...@@ -5,5 +5,5 @@
"""Spacemouse device for SE(2) and SE(3) control.""" """Spacemouse device for SE(2) and SE(3) control."""
from .se2_spacemouse import Se2SpaceMouse from .se2_spacemouse import Se2SpaceMouse, Se2SpaceMouseCfg
from .se3_spacemouse import Se3SpaceMouse from .se3_spacemouse import Se3SpaceMouse, Se3SpaceMouseCfg
...@@ -9,12 +9,26 @@ import hid ...@@ -9,12 +9,26 @@ import hid
import numpy as np import numpy as np
import threading import threading
import time import time
import torch
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass
from ..device_base import DeviceBase from isaaclab.utils.array import convert_to_torch
from ..device_base import DeviceBase, DeviceCfg
from .utils import convert_buffer from .utils import convert_buffer
@dataclass
class Se2SpaceMouseCfg(DeviceCfg):
"""Configuration for SE2 space mouse devices."""
v_x_sensitivity: float = 0.8
v_y_sensitivity: float = 0.4
omega_z_sensitivity: float = 1.0
sim_device: str = "cpu"
class Se2SpaceMouse(DeviceBase): class Se2SpaceMouse(DeviceBase):
r"""A space-mouse controller for sending SE(2) commands as delta poses. r"""A space-mouse controller for sending SE(2) commands as delta poses.
...@@ -34,18 +48,17 @@ class Se2SpaceMouse(DeviceBase): ...@@ -34,18 +48,17 @@ class Se2SpaceMouse(DeviceBase):
""" """
def __init__(self, v_x_sensitivity: float = 0.8, v_y_sensitivity: float = 0.4, omega_z_sensitivity: float = 1.0): def __init__(self, cfg: Se2SpaceMouseCfg):
"""Initialize the spacemouse layer. """Initialize the spacemouse layer.
Args: Args:
v_x_sensitivity: Magnitude of linear velocity along x-direction scaling. Defaults to 0.8. cfg: Configuration for the spacemouse device.
v_y_sensitivity: Magnitude of linear velocity along y-direction scaling. Defaults to 0.4.
omega_z_sensitivity: Magnitude of angular velocity along z-direction scaling. Defaults to 1.0.
""" """
# store inputs # store inputs
self.v_x_sensitivity = v_x_sensitivity self.v_x_sensitivity = cfg.v_x_sensitivity
self.v_y_sensitivity = v_y_sensitivity self.v_y_sensitivity = cfg.v_y_sensitivity
self.omega_z_sensitivity = omega_z_sensitivity self.omega_z_sensitivity = cfg.omega_z_sensitivity
self._sim_device = cfg.sim_device
# acquire device interface # acquire device interface
self._device = hid.device() self._device = hid.device()
self._find_device() self._find_device()
...@@ -88,13 +101,13 @@ class Se2SpaceMouse(DeviceBase): ...@@ -88,13 +101,13 @@ class Se2SpaceMouse(DeviceBase):
# TODO: Improve this to allow multiple buttons on same key. # TODO: Improve this to allow multiple buttons on same key.
self._additional_callbacks[key] = func self._additional_callbacks[key] = func
def advance(self) -> np.ndarray: def advance(self) -> torch.Tensor:
"""Provides the result from spacemouse event state. """Provides the result from spacemouse event state.
Returns: Returns:
A 3D array containing the linear (x,y) and angular velocity (z). A 3D tensor containing the linear (x,y) and angular velocity (z).
""" """
return self._base_command return convert_to_torch(self._base_command, device=self._sim_device)
""" """
Internal helpers. Internal helpers.
......
...@@ -9,13 +9,24 @@ import hid ...@@ -9,13 +9,24 @@ import hid
import numpy as np import numpy as np
import threading import threading
import time import time
import torch
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass
from scipy.spatial.transform import Rotation from scipy.spatial.transform import Rotation
from ..device_base import DeviceBase from ..device_base import DeviceBase, DeviceCfg
from .utils import convert_buffer from .utils import convert_buffer
@dataclass
class Se3SpaceMouseCfg(DeviceCfg):
"""Configuration for SE3 space mouse devices."""
pos_sensitivity: float = 0.4
rot_sensitivity: float = 0.8
retargeters: None = None
class Se3SpaceMouse(DeviceBase): class Se3SpaceMouse(DeviceBase):
"""A space-mouse controller for sending SE(3) commands as delta poses. """A space-mouse controller for sending SE(3) commands as delta poses.
...@@ -38,16 +49,16 @@ class Se3SpaceMouse(DeviceBase): ...@@ -38,16 +49,16 @@ class Se3SpaceMouse(DeviceBase):
""" """
def __init__(self, pos_sensitivity: float = 0.4, rot_sensitivity: float = 0.8): def __init__(self, cfg: Se3SpaceMouseCfg):
"""Initialize the space-mouse layer. """Initialize the space-mouse layer.
Args: Args:
pos_sensitivity: Magnitude of input position command scaling. Defaults to 0.4. cfg: Configuration object for space-mouse settings.
rot_sensitivity: Magnitude of scale input rotation commands scaling. Defaults to 0.8.
""" """
# store inputs # store inputs
self.pos_sensitivity = pos_sensitivity self.pos_sensitivity = cfg.pos_sensitivity
self.rot_sensitivity = rot_sensitivity self.rot_sensitivity = cfg.rot_sensitivity
self._sim_device = cfg.sim_device
# acquire device interface # acquire device interface
self._device = hid.device() self._device = hid.device()
self._find_device() self._find_device()
...@@ -99,15 +110,19 @@ class Se3SpaceMouse(DeviceBase): ...@@ -99,15 +110,19 @@ class Se3SpaceMouse(DeviceBase):
# TODO: Improve this to allow multiple buttons on same key. # TODO: Improve this to allow multiple buttons on same key.
self._additional_callbacks[key] = func self._additional_callbacks[key] = func
def advance(self) -> tuple[np.ndarray, bool]: def advance(self) -> torch.Tensor:
"""Provides the result from spacemouse event state. """Provides the result from spacemouse event state.
Returns: Returns:
A tuple containing the delta pose command and gripper commands. torch.Tensor: A 7-element tensor containing:
- delta pose: First 6 elements as [x, y, z, rx, ry, rz] in meters and radians.
- gripper command: Last element as a binary value (+1.0 for open, -1.0 for close).
""" """
rot_vec = Rotation.from_euler("XYZ", self._delta_rot).as_rotvec() rot_vec = Rotation.from_euler("XYZ", self._delta_rot).as_rotvec()
# if new command received, reset event flag to False until keyboard updated. delta_pose = np.concatenate([self._delta_pos, rot_vec])
return np.concatenate([self._delta_pos, rot_vec]), self._close_gripper gripper_value = -1.0 if self._close_gripper else 1.0
command = np.append(delta_pose, gripper_value)
return torch.tensor(command, dtype=torch.float32, device=self._sim_device)
""" """
Internal helpers. Internal helpers.
......
# Copyright (c) 2022-2025, The Isaac Lab Project Developers.
# All rights reserved.
#
# SPDX-License-Identifier: BSD-3-Clause
"""Factory to create teleoperation devices from configuration."""
import contextlib
import inspect
from collections.abc import Callable
import omni.log
from isaaclab.devices import DeviceBase, DeviceCfg
from isaaclab.devices.gamepad import Se2Gamepad, Se2GamepadCfg, Se3Gamepad, Se3GamepadCfg
from isaaclab.devices.keyboard import Se2Keyboard, Se2KeyboardCfg, Se3Keyboard, Se3KeyboardCfg
from isaaclab.devices.openxr.retargeters import (
GR1T2Retargeter,
GR1T2RetargeterCfg,
GripperRetargeter,
GripperRetargeterCfg,
Se3AbsRetargeter,
Se3AbsRetargeterCfg,
Se3RelRetargeter,
Se3RelRetargeterCfg,
)
from isaaclab.devices.retargeter_base import RetargeterBase, RetargeterCfg
from isaaclab.devices.spacemouse import Se2SpaceMouse, Se2SpaceMouseCfg, Se3SpaceMouse, Se3SpaceMouseCfg
with contextlib.suppress(ModuleNotFoundError):
# May fail if xr is not in use
from isaaclab.devices.openxr import OpenXRDevice, OpenXRDeviceCfg
# Map device types to their constructor and expected config type
DEVICE_MAP: dict[type[DeviceCfg], type[DeviceBase]] = {
Se3KeyboardCfg: Se3Keyboard,
Se3SpaceMouseCfg: Se3SpaceMouse,
Se3GamepadCfg: Se3Gamepad,
Se2KeyboardCfg: Se2Keyboard,
Se2GamepadCfg: Se2Gamepad,
Se2SpaceMouseCfg: Se2SpaceMouse,
OpenXRDeviceCfg: OpenXRDevice,
}
# Map configuration types to their corresponding retargeter classes
RETARGETER_MAP: dict[type[RetargeterCfg], type[RetargeterBase]] = {
Se3AbsRetargeterCfg: Se3AbsRetargeter,
Se3RelRetargeterCfg: Se3RelRetargeter,
GripperRetargeterCfg: GripperRetargeter,
GR1T2RetargeterCfg: GR1T2Retargeter,
}
def create_teleop_device(
device_name: str, devices_cfg: dict[str, DeviceCfg], callbacks: dict[str, Callable] | None = None
) -> DeviceBase:
"""Create a teleoperation device based on configuration.
Args:
device_name: The name of the device to create (must exist in devices_cfg)
devices_cfg: Dictionary of device configurations
callbacks: Optional dictionary of callbacks to register with the device
Keys are the button/gesture names, values are callback functions
Returns:
The configured teleoperation device
Raises:
ValueError: If the device name is not found in the configuration
ValueError: If the device configuration type is not supported
"""
if device_name not in devices_cfg:
raise ValueError(f"Device '{device_name}' not found in teleop device configurations")
device_cfg = devices_cfg[device_name]
callbacks = callbacks or {}
# Check if device config type is supported
cfg_type = type(device_cfg)
if cfg_type not in DEVICE_MAP:
raise ValueError(f"Unsupported device configuration type: {cfg_type.__name__}")
# Get the constructor for this config type
constructor = DEVICE_MAP[cfg_type]
# Try to create retargeters if they are configured
retargeters = []
if hasattr(device_cfg, "retargeters") and device_cfg.retargeters is not None:
try:
# Create retargeters based on configuration
for retargeter_cfg in device_cfg.retargeters:
cfg_type = type(retargeter_cfg)
if cfg_type in RETARGETER_MAP:
retargeters.append(RETARGETER_MAP[cfg_type](retargeter_cfg))
else:
raise ValueError(f"Unknown retargeter configuration type: {cfg_type.__name__}")
except NameError as e:
raise ValueError(f"Failed to create retargeters: {e}")
# Check if the constructor accepts retargeters parameter
constructor_params = inspect.signature(constructor).parameters
if "retargeters" in constructor_params and retargeters:
device = constructor(cfg=device_cfg, retargeters=retargeters)
else:
device = constructor(cfg=device_cfg)
# Register callbacks
for key, callback in callbacks.items():
device.add_callback(key, callback)
omni.log.info(f"Created teleoperation device: {device_name}")
return device
...@@ -9,9 +9,10 @@ This module defines the general configuration of the environment. It includes pa ...@@ -9,9 +9,10 @@ This module defines the general configuration of the environment. It includes pa
configuring the environment instances, viewer settings, and simulation parameters. configuring the environment instances, viewer settings, and simulation parameters.
""" """
from dataclasses import MISSING from dataclasses import MISSING, field
import isaaclab.envs.mdp as mdp import isaaclab.envs.mdp as mdp
from isaaclab.devices.device_base import DevicesCfg
from isaaclab.devices.openxr import XrCfg from isaaclab.devices.openxr import XrCfg
from isaaclab.managers import EventTermCfg as EventTerm from isaaclab.managers import EventTermCfg as EventTerm
from isaaclab.managers import RecorderManagerBaseCfg as DefaultEmptyRecorderManagerCfg from isaaclab.managers import RecorderManagerBaseCfg as DefaultEmptyRecorderManagerCfg
...@@ -121,3 +122,6 @@ class ManagerBasedEnvCfg: ...@@ -121,3 +122,6 @@ class ManagerBasedEnvCfg:
xr: XrCfg | None = None xr: XrCfg | None = None
"""Configuration for viewing and interacting with the environment through an XR device.""" """Configuration for viewing and interacting with the environment through an XR device."""
teleop_devices: DevicesCfg = field(default_factory=DevicesCfg)
"""Configuration for teleoperation devices."""
...@@ -25,7 +25,7 @@ import ctypes ...@@ -25,7 +25,7 @@ import ctypes
from isaacsim.core.api.simulation_context import SimulationContext from isaacsim.core.api.simulation_context import SimulationContext
from isaaclab.devices import Se3Keyboard from isaaclab.devices import Se3Keyboard, Se3KeyboardCfg
def print_cb(): def print_cb():
...@@ -44,7 +44,7 @@ def main(): ...@@ -44,7 +44,7 @@ def main():
sim = SimulationContext(physics_dt=0.01, rendering_dt=0.01) sim = SimulationContext(physics_dt=0.01, rendering_dt=0.01)
# Create teleoperation interface # Create teleoperation interface
teleop_interface = Se3Keyboard(pos_sensitivity=0.1, rot_sensitivity=0.1) teleop_interface = Se3Keyboard(Se3KeyboardCfg(pos_sensitivity=0.1, rot_sensitivity=0.1))
# Add teleoperation callbacks # Add teleoperation callbacks
# available key buttons: https://docs.omniverse.nvidia.com/kit/docs/carbonite/latest/docs/python/carb.html?highlight=keyboardeventtype#carb.input.KeyboardInput # available key buttons: https://docs.omniverse.nvidia.com/kit/docs/carbonite/latest/docs/python/carb.html?highlight=keyboardeventtype#carb.input.KeyboardInput
teleop_interface.add_callback("L", print_cb) teleop_interface.add_callback("L", print_cb)
......
This diff is collapsed.
...@@ -7,6 +7,9 @@ from pink.tasks import FrameTask ...@@ -7,6 +7,9 @@ from pink.tasks import FrameTask
import isaaclab.controllers.utils as ControllerUtils import isaaclab.controllers.utils as ControllerUtils
from isaaclab.controllers.pink_ik_cfg import PinkIKControllerCfg from isaaclab.controllers.pink_ik_cfg import PinkIKControllerCfg
from isaaclab.devices import DevicesCfg
from isaaclab.devices.openxr import OpenXRDeviceCfg
from isaaclab.devices.openxr.retargeters import GR1T2RetargeterCfg
from isaaclab.envs.mdp.actions.pink_actions_cfg import PinkInverseKinematicsActionCfg from isaaclab.envs.mdp.actions.pink_actions_cfg import PinkInverseKinematicsActionCfg
from isaaclab.utils import configclass from isaaclab.utils import configclass
...@@ -152,3 +155,21 @@ class ExhaustPipeGR1T2PinkIKEnvCfg(ExhaustPipeGR1T2BaseEnvCfg): ...@@ -152,3 +155,21 @@ class ExhaustPipeGR1T2PinkIKEnvCfg(ExhaustPipeGR1T2BaseEnvCfg):
# Set the URDF and mesh paths for the IK controller # Set the URDF and mesh paths for the IK controller
self.actions.gr1_action.controller.urdf_path = temp_urdf_output_path self.actions.gr1_action.controller.urdf_path = temp_urdf_output_path
self.actions.gr1_action.controller.mesh_path = temp_urdf_meshes_output_path self.actions.gr1_action.controller.mesh_path = temp_urdf_meshes_output_path
self.teleop_devices = DevicesCfg(
devices={
"handtracking": OpenXRDeviceCfg(
retargeters=[
GR1T2RetargeterCfg(
enable_visualization=True,
# OpenXR hand tracking has 26 joints per hand
num_open_xr_hand_joints=2 * 26,
sim_device=self.sim.device,
hand_joint_names=self.actions.gr1_action.hand_joint_names,
),
],
sim_device=self.sim.device,
xr_cfg=self.xr,
),
}
)
...@@ -7,6 +7,9 @@ from pink.tasks import FrameTask ...@@ -7,6 +7,9 @@ from pink.tasks import FrameTask
import isaaclab.controllers.utils as ControllerUtils import isaaclab.controllers.utils as ControllerUtils
from isaaclab.controllers.pink_ik_cfg import PinkIKControllerCfg from isaaclab.controllers.pink_ik_cfg import PinkIKControllerCfg
from isaaclab.devices import DevicesCfg
from isaaclab.devices.openxr import OpenXRDeviceCfg
from isaaclab.devices.openxr.retargeters import GR1T2RetargeterCfg
from isaaclab.envs.mdp.actions.pink_actions_cfg import PinkInverseKinematicsActionCfg from isaaclab.envs.mdp.actions.pink_actions_cfg import PinkInverseKinematicsActionCfg
from isaaclab.utils import configclass from isaaclab.utils import configclass
...@@ -150,3 +153,21 @@ class NutPourGR1T2PinkIKEnvCfg(NutPourGR1T2BaseEnvCfg): ...@@ -150,3 +153,21 @@ class NutPourGR1T2PinkIKEnvCfg(NutPourGR1T2BaseEnvCfg):
# Set the URDF and mesh paths for the IK controller # Set the URDF and mesh paths for the IK controller
self.actions.gr1_action.controller.urdf_path = temp_urdf_output_path self.actions.gr1_action.controller.urdf_path = temp_urdf_output_path
self.actions.gr1_action.controller.mesh_path = temp_urdf_meshes_output_path self.actions.gr1_action.controller.mesh_path = temp_urdf_meshes_output_path
self.teleop_devices = DevicesCfg(
devices={
"handtracking": OpenXRDeviceCfg(
retargeters=[
GR1T2RetargeterCfg(
enable_visualization=True,
# OpenXR hand tracking has 26 joints per hand
num_open_xr_hand_joints=2 * 26,
sim_device=self.sim.device,
hand_joint_names=self.actions.gr1_action.hand_joint_names,
),
],
sim_device=self.sim.device,
xr_cfg=self.xr,
),
}
)
...@@ -18,7 +18,9 @@ import isaaclab.envs.mdp as base_mdp ...@@ -18,7 +18,9 @@ import isaaclab.envs.mdp as base_mdp
import isaaclab.sim as sim_utils import isaaclab.sim as sim_utils
from isaaclab.assets import ArticulationCfg, AssetBaseCfg, RigidObjectCfg from isaaclab.assets import ArticulationCfg, AssetBaseCfg, RigidObjectCfg
from isaaclab.controllers.pink_ik_cfg import PinkIKControllerCfg from isaaclab.controllers.pink_ik_cfg import PinkIKControllerCfg
from isaaclab.devices.openxr import XrCfg from isaaclab.devices.device_base import DevicesCfg
from isaaclab.devices.openxr import OpenXRDeviceCfg, XrCfg
from isaaclab.devices.openxr.retargeters.humanoid.fourier.gr1t2_retargeter import GR1T2RetargeterCfg
from isaaclab.envs import ManagerBasedRLEnvCfg from isaaclab.envs import ManagerBasedRLEnvCfg
from isaaclab.envs.mdp.actions.pink_actions_cfg import PinkInverseKinematicsActionCfg from isaaclab.envs.mdp.actions.pink_actions_cfg import PinkInverseKinematicsActionCfg
from isaaclab.managers import EventTermCfg as EventTerm from isaaclab.managers import EventTermCfg as EventTerm
...@@ -410,3 +412,21 @@ class PickPlaceGR1T2EnvCfg(ManagerBasedRLEnvCfg): ...@@ -410,3 +412,21 @@ class PickPlaceGR1T2EnvCfg(ManagerBasedRLEnvCfg):
# Set the URDF and mesh paths for the IK controller # Set the URDF and mesh paths for the IK controller
self.actions.pink_ik_cfg.controller.urdf_path = temp_urdf_output_path self.actions.pink_ik_cfg.controller.urdf_path = temp_urdf_output_path
self.actions.pink_ik_cfg.controller.mesh_path = temp_urdf_meshes_output_path self.actions.pink_ik_cfg.controller.mesh_path = temp_urdf_meshes_output_path
self.teleop_devices = DevicesCfg(
devices={
"handtracking": OpenXRDeviceCfg(
retargeters=[
GR1T2RetargeterCfg(
enable_visualization=True,
# OpenXR hand tracking has 26 joints per hand
num_open_xr_hand_joints=2 * 26,
sim_device=self.sim.device,
hand_joint_names=self.actions.pink_ik_cfg.hand_joint_names,
),
],
sim_device=self.sim.device,
xr_cfg=self.xr,
),
}
)
...@@ -4,6 +4,10 @@ ...@@ -4,6 +4,10 @@
# SPDX-License-Identifier: BSD-3-Clause # SPDX-License-Identifier: BSD-3-Clause
from isaaclab.controllers.differential_ik_cfg import DifferentialIKControllerCfg from isaaclab.controllers.differential_ik_cfg import DifferentialIKControllerCfg
from isaaclab.devices.device_base import DevicesCfg
from isaaclab.devices.openxr.openxr_device import OpenXRDevice, OpenXRDeviceCfg
from isaaclab.devices.openxr.retargeters.manipulator.gripper_retargeter import GripperRetargeterCfg
from isaaclab.devices.openxr.retargeters.manipulator.se3_abs_retargeter import Se3AbsRetargeterCfg
from isaaclab.envs.mdp.actions.actions_cfg import DifferentialInverseKinematicsActionCfg from isaaclab.envs.mdp.actions.actions_cfg import DifferentialInverseKinematicsActionCfg
from isaaclab.utils import configclass from isaaclab.utils import configclass
...@@ -32,3 +36,24 @@ class FrankaCubeStackEnvCfg(stack_joint_pos_env_cfg.FrankaCubeStackEnvCfg): ...@@ -32,3 +36,24 @@ class FrankaCubeStackEnvCfg(stack_joint_pos_env_cfg.FrankaCubeStackEnvCfg):
body_name="panda_hand", body_name="panda_hand",
controller=DifferentialIKControllerCfg(command_type="pose", use_relative_mode=False, ik_method="dls"), controller=DifferentialIKControllerCfg(command_type="pose", use_relative_mode=False, ik_method="dls"),
) )
self.teleop_devices = DevicesCfg(
devices={
"handtracking": OpenXRDeviceCfg(
retargeters=[
Se3AbsRetargeterCfg(
bound_hand=OpenXRDevice.TrackingTarget.HAND_RIGHT,
zero_out_xy_rotation=True,
use_wrist_rotation=False,
use_wrist_position=True,
sim_device=self.sim.device,
),
GripperRetargeterCfg(
bound_hand=OpenXRDevice.TrackingTarget.HAND_RIGHT, sim_device=self.sim.device
),
],
sim_device=self.sim.device,
xr_cfg=self.xr,
),
}
)
...@@ -4,6 +4,11 @@ ...@@ -4,6 +4,11 @@
# SPDX-License-Identifier: BSD-3-Clause # SPDX-License-Identifier: BSD-3-Clause
from isaaclab.controllers.differential_ik_cfg import DifferentialIKControllerCfg from isaaclab.controllers.differential_ik_cfg import DifferentialIKControllerCfg
from isaaclab.devices.device_base import DevicesCfg
from isaaclab.devices.keyboard import Se3KeyboardCfg
from isaaclab.devices.openxr.openxr_device import OpenXRDevice, OpenXRDeviceCfg
from isaaclab.devices.openxr.retargeters.manipulator.gripper_retargeter import GripperRetargeterCfg
from isaaclab.devices.openxr.retargeters.manipulator.se3_rel_retargeter import Se3RelRetargeterCfg
from isaaclab.envs.mdp.actions.actions_cfg import DifferentialInverseKinematicsActionCfg from isaaclab.envs.mdp.actions.actions_cfg import DifferentialInverseKinematicsActionCfg
from isaaclab.utils import configclass from isaaclab.utils import configclass
...@@ -34,3 +39,31 @@ class FrankaCubeStackEnvCfg(stack_joint_pos_env_cfg.FrankaCubeStackEnvCfg): ...@@ -34,3 +39,31 @@ class FrankaCubeStackEnvCfg(stack_joint_pos_env_cfg.FrankaCubeStackEnvCfg):
scale=0.5, scale=0.5,
body_offset=DifferentialInverseKinematicsActionCfg.OffsetCfg(pos=[0.0, 0.0, 0.107]), body_offset=DifferentialInverseKinematicsActionCfg.OffsetCfg(pos=[0.0, 0.0, 0.107]),
) )
self.teleop_devices = DevicesCfg(
devices={
"handtracking": OpenXRDeviceCfg(
retargeters=[
Se3RelRetargeterCfg(
bound_hand=OpenXRDevice.TrackingTarget.HAND_RIGHT,
zero_out_xy_rotation=True,
use_wrist_rotation=False,
use_wrist_position=True,
delta_pos_scale_factor=10.0,
delta_rot_scale_factor=10.0,
sim_device=self.sim.device,
),
GripperRetargeterCfg(
bound_hand=OpenXRDevice.TrackingTarget.HAND_RIGHT, sim_device=self.sim.device
),
],
sim_device=self.sim.device,
xr_cfg=self.xr,
),
"keyboard": Se3KeyboardCfg(
pos_sensitivity=0.05,
rot_sensitivity=0.05,
sim_device=self.sim.device,
),
}
)
...@@ -33,6 +33,7 @@ PER_TEST_TIMEOUTS = { ...@@ -33,6 +33,7 @@ PER_TEST_TIMEOUTS = {
"test_sb3_wrapper.py": 200, "test_sb3_wrapper.py": 200,
"test_skrl_wrapper.py": 200, "test_skrl_wrapper.py": 200,
"test_operational_space.py": 300, "test_operational_space.py": 300,
"test_device_constructors.py": 200,
"test_terrain_importer.py": 200, "test_terrain_importer.py": 200,
"test_environments_training.py": 5000, "test_environments_training.py": 5000,
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment