Retargeters: SO-101 (5-DOF arm)#
The SO-101 is a low-cost 5-DOF arm with a single-jaw gripper. The controller is meant to feel
like holding the leader arm: the gripper pose follows the controller pose directly. A
full SE3 pose is commanded and a single differential IK solves all 5 arm joints, tracking
position exactly and orientation best-effort (a 5-DOF arm is over-determined by one DOF on a
6-DOF pose). These two retargeters provide the pieces for comfortable XR controller
teleoperation of the arm (used by the Isaac Lab Isaac-Stack-Cube-SO101-IK-Abs-v0
cube-stacking task):
SO101ClutchRetargeter– absolute EE pose (position + orientation) with clutch-style position rebasing (no teleport on engage).SO101GripperRetargeter– proportional (analog) jaw closedness from the controller trigger.
Together they flatten (via TensorReorderer) into an 8-D action
[pos_x, pos_y, pos_z, quat_x, quat_y, quat_z, quat_w, gripper].
At a glance#
Retargeter |
Output |
What it does |
|---|---|---|
|
7-D |
Same output contract as |
|
1 float |
Trigger -> jaw closedness ( |
Why a clutch#
Se3AbsRetargeter maps the controller’s absolute position straight to the EE target, so
engaging teleop teleports the arm to wherever the controller happens to be. The clutch instead
re-arms whenever teleop is not RUNNING and latches a controller origin p0 on the
first RUNNING frame (the headset “Play”). From then on the EE position is driven by the
delta relative to p0, so engaging with a steady controller does not move the arm. On the
latching frame p_ctrl == p0, so the emitted position is exactly the home (no jump). The last
pose is held on a dropped frame.
The clutch keeps position-control IK (use_relative_mode=False): it emits an absolute
target, just rebased.
Frames and the home#
The controller stream reaching the clutch is already expressed in the robot base frame: the
Isaac Lab IsaacTeleopDevice rebases it upstream via its target_frame_prim_path (set to
the robot base), composing base_T_world onto the XR anchor before the controllers are
transformed. The clutch therefore applies the controller delta to the home directly, with no
world->base rotation of its own, and needs no live end-effector or base feed.
The home is the clutch’s own running home: it is seeded on reset / first engage from the
static home_base_T_ee reset-origin (the gripper’s pose in the base frame at the reset pose,
only its translation is used), and thereafter holds the last commanded pose so a mid-task
re-clutch resumes from where the arm was left.
Note
The fallback home, the position sign/scale knobs, and the orientation calibration offset
carry TODO(tune-in-sim) markers: the rebasing math is exact and unit-tested, but the
end-to-end controller->EE handedness and the neutral-controller -> neutral-gripper offset
should be confirmed in simulation.
Orientation calibration#
The clutch emits the controller grip orientation composed with a fixed calibration offset:
q_cmd = orientation_offset (x) q_grip (base-frame left multiply, renormalized). The offset
defaults to identity (passthrough); a non-identity offset maps a neutrally-held controller to a
sensible neutral gripper orientation for the SE3 IK. This single rotational offset replaces the
per-DOF roll/pitch calibration hacks of earlier revisions.
Gripper#
SO101GripperRetargeter maps the analog trigger to a jaw closedness c in [0, 1]
(0 = open, 1 = closed) with a small released-end deadzone, so a half-pressed trigger
leaves the jaw half-closed. Downstream, an order-locked JointPositionActionCfg applies the
affine joint = offset + scale * c mapping c onto the open/close joint angles. This is
deliberately independent of the shared GripperRetargeter’s
binary +1 = open / -1 = closed sign.
Use it from Python#
from isaacteleop.retargeters import (
SO101ClutchRetargeter,
SO101GripperRetargeter,
TensorReorderer,
)
from isaacteleop.retargeting_engine.deviceio_source_nodes import ControllersSource
from isaacteleop.retargeting_engine.interface import OutputCombiner, ValueInput
from isaacteleop.retargeting_engine.tensor_types import TransformMatrix
def build_so101_stack_pipeline():
controllers = ControllersSource(name="controllers")
world_T_anchor = ValueInput("world_T_anchor", TransformMatrix())
# The device rebases controller poses into the robot base frame upstream via
# target_frame_prim_path, so the clutch needs no live EE / base feed.
xformed = controllers.transformed(world_T_anchor.output(ValueInput.VALUE))
clutch = SO101ClutchRetargeter(name="ee_pose", input_device=ControllersSource.RIGHT)
connected_clutch = clutch.connect({
ControllersSource.RIGHT: xformed.output(ControllersSource.RIGHT),
})
gripper = SO101GripperRetargeter(name="gripper", input_device=ControllersSource.RIGHT)
connected_gripper = gripper.connect(
{ControllersSource.RIGHT: xformed.output(ControllersSource.RIGHT)}
)
# Keep all 7 pose names and pass the full pose (xyz + quat) plus gripper through.
ee_elements = ["pos_x", "pos_y", "pos_z", "quat_x", "quat_y", "quat_z", "quat_w"]
reorderer = TensorReorderer(
input_config={
"ee_pose": ee_elements,
"gripper_command": ["gripper_value"],
},
output_order=ee_elements + ["gripper_value"],
name="action_reorderer",
input_types={"ee_pose": "array", "gripper_command": "scalar"},
)
connected = reorderer.connect({
"ee_pose": connected_clutch.output("ee_pose"),
"gripper_command": connected_gripper.output("gripper_command"),
})
return OutputCombiner({"action": connected.output("output")})
See Build a Retargeting Pipeline for the general pipeline-builder pattern and Retargeting Interface for the full retargeting interface.
Validate#
The retargeters ship with sim-free unit tests (trigger/clutch math plus per-frame
compute behavior):
$ ctest --test-dir build -R retargeting_test_so101_retargeters --output-on-failure