Skip to content

Commit 30a470c

Browse files
committed
Adding Arean G1 locomanipulation retargeters
1 parent 017a1ee commit 30a470c

File tree

5 files changed

+537
-1
lines changed

5 files changed

+537
-1
lines changed

source/isaaclab/config/extension.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22

33
# Note: Semantic Versioning is used: https://semver.org/
4-
version = "0.49.0"
4+
version = "0.49.1"
55

66
# Description
77
title = "Isaac Lab framework for Robot Learning"

source/isaaclab/docs/CHANGELOG.rst

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
Changelog
22
---------
33

4+
0.49.1 (2025-12-03)
5+
~~~~~~~~~~~~~~~~~~~
6+
7+
Added
8+
^^^^^
9+
10+
* Added :class:`G1TriHandUpperBodyMotionControllerGripperRetargeter` and :class:`G1TriHandUpperBodyMotionControllerGripperRetargeterCfg` for retargeting the gripper state from motion controllers.
11+
* Added unit tests for the retargeters.
12+
13+
414
0.49.0 (2025-11-10)
515
~~~~~~~~~~~~~~~~~~~
616

source/isaaclab/isaaclab/devices/openxr/retargeters/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
G1LowerBodyStandingMotionControllerRetargeterCfg,
1212
)
1313
from .humanoid.unitree.inspire.g1_upper_body_retargeter import UnitreeG1Retargeter, UnitreeG1RetargeterCfg
14+
from .humanoid.unitree.trihand.g1_upper_body_motion_ctrl_gripper import (
15+
G1TriHandUpperBodyMotionControllerGripperRetargeter,
16+
G1TriHandUpperBodyMotionControllerGripperRetargeterCfg,
17+
)
1418
from .humanoid.unitree.trihand.g1_upper_body_motion_ctrl_retargeter import (
1519
G1TriHandUpperBodyMotionControllerRetargeter,
1620
G1TriHandUpperBodyMotionControllerRetargeterCfg,
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
# Copyright (c) 2022-2025, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
2+
# All rights reserved.
3+
#
4+
# SPDX-License-Identifier: BSD-3-Clause
5+
6+
from __future__ import annotations
7+
8+
import numpy as np
9+
import torch
10+
from dataclasses import dataclass
11+
12+
import isaaclab.utils.math as PoseUtils
13+
from isaaclab.devices.device_base import DeviceBase
14+
from isaaclab.devices.retargeter_base import RetargeterBase, RetargeterCfg
15+
16+
17+
class G1TriHandUpperBodyMotionControllerGripperRetargeter(RetargeterBase):
18+
"""Retargeter for G1 gripper that outputs a boolean state based on controller trigger input,
19+
concatenated with the retargeted wrist pose.
20+
21+
Gripper:
22+
- Uses hysteresis to prevent flickering when the trigger is near the threshold.
23+
- Output is 0.0 for open, 1.0 for close.
24+
25+
Wrist:
26+
- Retargets absolute pose from controller to robot frame.
27+
- Applies a fixed offset rotation for comfort/alignment.
28+
"""
29+
30+
def __init__(self, cfg: G1TriHandUpperBodyMotionControllerGripperRetargeterCfg):
31+
"""Initialize the retargeter.
32+
33+
Args:
34+
cfg: Configuration for the retargeter.
35+
"""
36+
super().__init__(cfg)
37+
self._cfg = cfg
38+
# Track previous state for hysteresis (left, right)
39+
self._prev_left_state: float = 0.0
40+
self._prev_right_state: float = 0.0
41+
42+
def retarget(self, data: dict) -> torch.Tensor:
43+
"""Retarget controller inputs to gripper boolean state and wrist pose.
44+
45+
Args:
46+
data: Dictionary with MotionControllerTrackingTarget.LEFT/RIGHT keys
47+
Each value is a 2D array: [pose(7), inputs(7)]
48+
49+
Returns:
50+
Tensor: [left_gripper_state(1), right_gripper_state(1), left_wrist(7), right_wrist(7)]
51+
Wrist format: [x, y, z, qw, qx, qy, qz]
52+
"""
53+
# Get controller data
54+
left_controller_data = data.get(DeviceBase.TrackingTarget.CONTROLLER_LEFT, np.array([]))
55+
right_controller_data = data.get(DeviceBase.TrackingTarget.CONTROLLER_RIGHT, np.array([]))
56+
57+
# --- Gripper Logic ---
58+
# Extract hand state from controller data with hysteresis
59+
left_hand_state: float = self._extract_hand_state(left_controller_data, self._prev_left_state)
60+
right_hand_state: float = self._extract_hand_state(right_controller_data, self._prev_right_state)
61+
62+
# Update previous states
63+
self._prev_left_state = left_hand_state
64+
self._prev_right_state = right_hand_state
65+
66+
gripper_tensor = torch.tensor([left_hand_state, right_hand_state], dtype=torch.float32, device=self._sim_device)
67+
68+
# --- Wrist Logic ---
69+
# Default wrist poses (position + quaternion [w, x, y, z] as per default_wrist init)
70+
# Note: default_wrist is [x, y, z, w, x, y, z] in reference, but seemingly used as [x,y,z, w,x,y,z]
71+
default_wrist = np.array([0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0])
72+
73+
# Extract poses from controller data
74+
left_wrist = self._extract_wrist_pose(left_controller_data, default_wrist)
75+
right_wrist = self._extract_wrist_pose(right_controller_data, default_wrist)
76+
77+
# Convert to tensors
78+
left_wrist_tensor = torch.tensor(self._retarget_abs(left_wrist), dtype=torch.float32, device=self._sim_device)
79+
right_wrist_tensor = torch.tensor(self._retarget_abs(right_wrist), dtype=torch.float32, device=self._sim_device)
80+
81+
# Concatenate: [gripper(2), left_wrist(7), right_wrist(7)]
82+
return torch.cat([gripper_tensor, left_wrist_tensor, right_wrist_tensor])
83+
84+
def _extract_hand_state(self, controller_data: np.ndarray, prev_state: float) -> float:
85+
"""Extract hand state from controller data with hysteresis.
86+
87+
Args:
88+
controller_data: 2D array [pose(7), inputs(7)]
89+
prev_state: Previous hand state (0.0 or 1.0)
90+
91+
Returns:
92+
Hand state as float (0.0 for open, 1.0 for close)
93+
"""
94+
if len(controller_data) <= DeviceBase.MotionControllerDataRowIndex.INPUTS.value:
95+
return 0.0
96+
97+
# Extract inputs from second row
98+
inputs = controller_data[DeviceBase.MotionControllerDataRowIndex.INPUTS.value]
99+
if len(inputs) < len(DeviceBase.MotionControllerInputIndex):
100+
return 0.0
101+
102+
# Extract specific inputs using enum
103+
trigger = inputs[DeviceBase.MotionControllerInputIndex.TRIGGER.value] # 0.0 to 1.0 (analog)
104+
105+
# Apply hysteresis
106+
if prev_state < 0.5: # Currently open
107+
return 1.0 if trigger > self._cfg.threshold_high else 0.0
108+
else: # Currently closed
109+
return 0.0 if trigger < self._cfg.threshold_low else 1.0
110+
111+
def _extract_wrist_pose(self, controller_data: np.ndarray, default_pose: np.ndarray) -> np.ndarray:
112+
"""Extract wrist pose from controller data.
113+
114+
Args:
115+
controller_data: 2D array [pose(7), inputs(7)]
116+
default_pose: Default pose to use if no data
117+
118+
Returns:
119+
Wrist pose array [x, y, z, w, x, y, z]
120+
"""
121+
if len(controller_data) > DeviceBase.MotionControllerDataRowIndex.POSE.value:
122+
return controller_data[DeviceBase.MotionControllerDataRowIndex.POSE.value]
123+
return default_pose
124+
125+
def _retarget_abs(self, wrist: np.ndarray) -> np.ndarray:
126+
"""Handle absolute pose retargeting for controller wrists."""
127+
wrist_pos = torch.tensor(wrist[:3], dtype=torch.float32)
128+
wrist_quat = torch.tensor(wrist[3:], dtype=torch.float32)
129+
130+
# Combined -75° (rather than -90° for wrist comfort) Y rotation + 90° Z rotation
131+
# This is equivalent to (0, -75, 90) in euler angles
132+
combined_quat = torch.tensor([0.5358, -0.4619, 0.5358, 0.4619], dtype=torch.float32)
133+
134+
openxr_pose = PoseUtils.make_pose(wrist_pos, PoseUtils.matrix_from_quat(wrist_quat))
135+
transform_pose = PoseUtils.make_pose(torch.zeros(3), PoseUtils.matrix_from_quat(combined_quat))
136+
137+
result_pose = PoseUtils.pose_in_A_to_pose_in_B(transform_pose, openxr_pose)
138+
pos, rot_mat = PoseUtils.unmake_pose(result_pose)
139+
quat = PoseUtils.quat_from_matrix(rot_mat)
140+
141+
return np.concatenate([pos.numpy(), quat.numpy()])
142+
143+
def get_requirements(self) -> list[RetargeterBase.Requirement]:
144+
return [RetargeterBase.Requirement.MOTION_CONTROLLER]
145+
146+
147+
@dataclass
148+
class G1TriHandUpperBodyMotionControllerGripperRetargeterCfg(RetargeterCfg):
149+
"""Configuration for the G1 boolean gripper and wrist retargeter."""
150+
151+
threshold_high: float = 0.6 # Threshold to close hand
152+
threshold_low: float = 0.4 # Threshold to open hand
153+
retargeter_type: type[RetargeterBase] = G1TriHandUpperBodyMotionControllerGripperRetargeter

0 commit comments

Comments
 (0)