You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This document describes the integration plan for using NVIDIA Isaac Teleop as a teleoperation backend within the LeRobot framework. The goal is to implement new LeRobot Teleoperator subclasses that wrap Isaac Teleop's TeleopSession, exposing its diverse input devices — XR (VR controllers, hand tracking, full-body tracking), peripheral (foot pedals, Manus gloves), and custom sources — as first-class teleop backends in LeRobot's recording and control pipelines.
Motivation
LeRobot's existing teleop backends are predominantly hardware-specific leader arms (SO100, Koch), phones, and gamepads. Isaac Teleop brings:
Diverse device support:
XR devices (VR controllers, hand tracking, head pose, full-body tracking) via OpenXR
non-XR peripherals (foot pedals, Manus gloves, joysticks) via the plugin system
Retargeting engine: A graph-based pipeline for transforming raw input data from any source into robot-specific commands (SE3 poses, dexterous hand joints, gripper commands, locomotion velocities)
Extensible device abstraction: Plugin architecture allows adding new input devices — both XR and non-XR — without modifying core code
Existing LeRobot bridge: A minimal examples/lerobot/record.py in the Teleop repo already demonstrates recording tracking data to LeRobot dataset format (head + wrist positions only)
This integration makes Isaac Teleop's full input pipeline — XR and non-XR alike — available through LeRobot's standard Teleoperator interface, enabling demonstration recording, leader-follower control, and policy training with a wide range of input devices.
block
columns 1
block:lerobot["LeRobot"]
columns 5
F["Robot.send_action()"]
space:1
E["Record loop / control pipeline<br/>ProcessorStep (EE → joints IK)"]
space:1
D["LeTeleop Teleoperator<br/>.get_action() → RobotAction"]
end
space:1
block:nvidia["NVIDIA Isaac Teleop"]
columns 5
A["Device Interface<br/>(VR headset, controllers,<br/>hand tracking, manus gloves)"]
space:1
B["Retargeting Pipeline<br/>(Se3, DexHand, Gripper,<br/>Locomotion)"]
space:1
C["TeleopSession.step() →<br/>Dict[str, OptionalTensorGroup]"]
end
A --> B
B --> C
C --> D
D --> E
E --> F
style A fill:#76b900,color:#fff
style B fill:#76b900,color:#fff
style C fill:#76b900,color:#fff
style D fill:#ff9d00,color:#fff
style E fill:#ff9d00,color:#fff
style F fill:#ff9d00,color:#fff
style nvidia fill:none,stroke:#76b900,stroke-width:2px
style lerobot fill:none,stroke:#ff9d00,stroke-width:2px
Loading
Components of Isaac Teleop
For LeRobot developers unfamiliar with Isaac Teleop, here is a quick tour of its layered architecture. Think of it as a pipeline: hardware sources produce raw tracking data, retargeters transform that data into robot-meaningful commands, and the session orchestrates the whole loop.
Sources (hardware → tensors)
Sources are stateless converters that poll hardware trackers and produce OptionalTensorGroup outputs. The built-in sources use OpenXR, but the architecture is not XR-only — plugins provide additional source types (e.g., foot pedals via Linux joystick, Manus gloves via Manus SDK). Each source auto-registers its tracker; you don't manage hardware directly.
Foot pedal (left, right, rudder) via Linux joystick
3 normalized scalars [-1, 1]
All outputs are OptionalTensorGroup — they have an .is_none property that is True when the input is unavailable (hand occluded, controller off, pedal disconnected, etc.). This is the Isaac Teleop equivalent of returning None.
Retargeters (tensors → robot commands)
Retargeters are pure-Python transform nodes that implement BaseRetargeter. Each declares its input_spec() and output_spec(), then implements _compute_fn(). They are wired together via .connect() to form a DAG.
LeRobot analogy: Retargeters are similar to LeRobot's ProcessorStep — both are chainable transforms. The difference is that Isaac Teleop retargeters run inside the teleop session (before get_action()), while LeRobot processors run after (between get_action() and send_action()).
OutputCombiner (merging outputs)
OutputCombiner takes named outputs from multiple sources and retargeters and merges them into a single dict. This is the "finalization" step that defines what TeleopSession.step() returns.
TeleopSession is the top-level context manager that ties everything together. On construction, it auto-discovers which sources and trackers the pipeline needs. Each call to .step():
Polls all hardware trackers (OpenXR and plugin-provided)
Feeds raw data through the retargeting pipeline
Returns the final Dict[str, OptionalTensorGroup]
LeRobot analogy:TeleopSession is conceptually the "inner loop" that our _IsaacTeleopBase.get_action() wraps. LeRobot developers never interact with TeleopSession directly — the IsaacTeleopController / IsaacTeleopHand subclasses handle it.
Plugins (optional hardware extensions)
Isaac Teleop supports plugins for devices beyond the built-in OpenXR sources:
Plugin
Purpose
Manus hand plugin
Manus glove hand tracking via Manus SDK
OAK camera
OAK camera video capture with H.264 encoding
3-axis pedal
Linux joystick foot pedal for locomotion input
Synthetic hands
Generates fake hand tracking from controller poses (useful for testing)
Where Retargeting Happens
Isaac Teleop has its own retargeting engine (raw tracking → robot commands). LeRobot has a ProcessorStep pipeline (action transforms, IK/FK). Two strategies:
Isaac-side retargeting (recommended for dexterous hands, locomotion): Configure Isaac Teleop's pipeline with appropriate retargeters (DexHandRetargeter, Se3AbsRetargeter, GripperRetargeter, etc.) so that TeleopSession.step() already outputs robot-ready joint positions. The LeRobot teleoperator simply maps these to RobotAction keys.
LeRobot-side retargeting (recommended for simple EE control of arms): Pass through raw SE3 poses from Isaac Teleop, then use LeRobot's existing EEReferenceAndDelta + InverseKinematicsEEToJoints processor steps to convert to joint targets for a specific follower robot.
Both approaches can coexist; the config determines which retargeters are in the Isaac pipeline.
Scope
In Scope (Phase 1: Core Integration)
Shared base class_IsaacTeleopBase: session lifecycle, plugin management, connect/disconnect
IsaacTeleopController ("isaac_teleop_controller"): VR controller aim pose + trigger/squeeze as gripper
IsaacTeleopHand ("isaac_teleop_hand"): Wrist pose + finger joint positions from hand tracking (OpenXR or Manus gloves)
Configuration per type: Separate TeleoperatorConfig subclass per semantic type
Locomotion commands: Joystick/pedal-based root velocity commands for mobile robots
Feedback channel: send_feedback() for haptic feedback via controllers (if supported by the device)
Camera streaming: OAK camera plugin integration for robot-eye-view in VR
Out of Scope
Modifying Isaac Teleop's core C++ layer or OpenXR runtime
ROS2 integration (Isaac Teleop already has a ROS2 example; this integration is Python-native)
Training/inference pipeline changes in LeRobot (the teleoperator is purely an input device)
CloudXR networking setup (deployment concern, not integration concern)
Teleoperator Subclassing Strategy
The Design Tension
Isaac Teleop's TeleopSession is fundamentally config-driven: one class handles all modes. The pipeline graph (sources + retargeters + OutputCombiner) determines both what hardware is active and what data flows out. Isaac Lab follows this pattern exactly — one IsaacTeleopDevice class, with a pipeline_builder callable in config that returns an OutputCombiner. Different robots and control modes are entirely config-level variations.
LeRobot's convention is different. Studying existing Teleoperator subclasses reveals a clear pattern for when subclasses are created:
Reason to subclass
Example
Why not config?
Different communication protocol
IOSPhone vs AndroidPhone (HEBI SDK vs WebXR)
Different threading model, SDK, connection logic
Different semantic output space
KeyboardTeleop vs KeyboardEndEffectorTeleop vs KeyboardRoverTeleop
action_features returns fundamentally different keys and types
Compositional relationship
BiSOLeader wraps two SOLeader instances
Different lifecycle, prefixes keys with left_/right_
Isaac Teleop modes produce fundamentally different output semantics — a controller returns SE3 pose + trigger, hand tracking returns 26 joint poses, full-body returns a skeleton, a foot pedal returns velocity scalars. These map to different action_features shapes. Following LeRobot convention, each semantic output type warrants its own subclass.
However, within a semantic type, pipeline configuration (which retargeters to use, which robot's URDF) is handled by config — not subclassing.
classBiIsaacTeleopController(Teleoperator):
"""Wraps two IsaacTeleopController instances (left + right)."""
Why Not a Single Class with a mode String?
A single IsaacTeleop class with mode: str = "controller" would work but conflicts with LeRobot conventions:
action_features would be mode-dependent — a consumer can't know the output shape without also knowing the mode. In LeRobot, action_features is a static contract per class.
Config registration — LeRobot uses @TeleoperatorConfig.register_subclass("type_name") for CLI dispatch (--teleop.type=isaac_teleop_controller). Separate types are more discoverable.
Precedent — Keyboard has three subclasses for three output types, not one class with a mode enum.
Why Not One Subclass Per Pipeline?
Going the other direction — one subclass per pipeline preset (e.g., IsaacTeleopDexHand, IsaacTeleopSe3Controller) — would be too granular. The retargeting choice changes the resolution of the output, not its semantic type. A controller with Se3AbsRetargeter still outputs {ee_pos, ee_quat, gripper} — the retargeter just transforms the coordinates. This matches the Reachy2 pattern: one class with config-driven internal variation.
defconvert_controller_to_action(result: RetargeterIO, config: IsaacTeleopConfig) ->RobotAction:
side=config.hand_sidecontroller=result[f"controller_{side}"]
ifcontroller.is_none:
raiseTrackingLostError(f"{side} controller not tracked")
action= {}
action["ee_pos"] =np.asarray(controller[ControllerInputIndex.AIM_POSITION]) # (3,)action["ee_quat"] =np.asarray(controller[ControllerInputIndex.AIM_ORIENTATION]) # (4,)# Gripper from trigger or squeezeifconfig.gripper_source=="trigger":
raw=float(controller[ControllerInputIndex.TRIGGER_VALUE])
else:
raw=float(controller[ControllerInputIndex.SQUEEZE_VALUE])
action["gripper"] =rawreturnaction
For retargeted outputs (e.g., dexterous hand joints), the conversion reads the retargeter's named output tensors and maps them to joint-named keys matching the target robot.
Pipeline Builders
Each subclass's _build_pipeline() creates an Isaac Teleop pipeline graph internally. The pipelines.py module provides builder functions:
Retargeters change the coordinate frame or resolution of the output, not its semantic type. A controller with Se3AbsRetargeter still outputs {ee_pos, ee_quat, gripper} — the retargeter maps poses from XR-space to robot-space.
fromlerobot.teleoperators.isaac_teleopimportIsaacTeleopController, IsaacTeleopControllerConfigfromlerobot.robots.so_followerimportSO100Follower, SO100FollowerConfigteleop_config=IsaacTeleopControllerConfig(hand_side="right")
robot_config=SO100FollowerConfig(port="/dev/ttyUSB0")
withIsaacTeleopController(teleop_config) asteleop, SO100Follower(robot_config) asrobot:
whileTrue:
action=teleop.get_action() # {"ee_pos": ..., "ee_quat": ..., "gripper": ...}# User must set up a processor pipeline for EE → joint conversionrobot.send_action(processed_action)
Dependencies
Python Version Compatibility
Project
Supported Python Versions
LeRobot
>= 3.12 (classifiers: 3.12, 3.13)
Isaac Teleop
3.10, 3.11, 3.12
Overlap
3.12 only
This is a significant constraint. The only compatible Python version today is 3.12.
Isaac Teleop does not support Python 3.13+ (LeRobot's secondary supported version). Isaac Teleop uses pybind11 C++ extensions that must be compiled per Python minor version, so adding 3.13 support requires a build/test pass on that version.
LeRobot does not support Python 3.10 or 3.11 — it uses 3.12+ syntax features (e.g., type statement, T | None in non-annotation contexts) and pins requires-python = ">=3.12".
Action required: Pin the integration to Python 3.12. Longer term, either extend Isaac Teleop's build matrix to include 3.13, or accept 3.12 as the single supported version for this integration.
Required
isaac_teleop Python package (built from /code/Teleop/ via CMake → wheel)
OpenXR runtime (SteamVR, Monado, or CloudXR) — required for XR sources; non-XR plugins work without it
numpy >= 2.0
Python 3.12 (the only version supported by both projects)
Return last-known action with a tracking_valid flag; let downstream decide hold/stop behavior
API version coupling
Isaac Teleop API changes break integration
Pin to a specific Isaac Teleop version; wrap low-level access in tensor_conversion.py for easy adaptation
Testing Strategy
Unit tests with synthetic hands: Isaac Teleop's synthetic hands plugin provides deterministic test input without XR hardware. Write tests that verify get_action() returns correct shapes and values.
Mock teleoperator: Extend LeRobot's existing MockTeleop pattern to simulate Isaac Teleop outputs.
Integration test: End-to-end recording test using synthetic hands → IsaacTeleop.get_action() → LeRobotDataset.add_frame().
Hardware-in-the-loop: Manual testing with actual devices — VR headset, foot pedals, Manus gloves (documented in test plan, not CI).
LeTeleop: Isaac Teleop Backend for LeRobot
Overview
This document describes the integration plan for using NVIDIA Isaac Teleop as a teleoperation backend within the LeRobot framework. The goal is to implement new LeRobot
Teleoperatorsubclasses that wrap Isaac Teleop'sTeleopSession, exposing its diverse input devices — XR (VR controllers, hand tracking, full-body tracking), peripheral (foot pedals, Manus gloves), and custom sources — as first-class teleop backends in LeRobot's recording and control pipelines.Motivation
LeRobot's existing teleop backends are predominantly hardware-specific leader arms (SO100, Koch), phones, and gamepads. Isaac Teleop brings:
examples/lerobot/record.pyin the Teleop repo already demonstrates recording tracking data to LeRobot dataset format (head + wrist positions only)This integration makes Isaac Teleop's full input pipeline — XR and non-XR alike — available through LeRobot's standard
Teleoperatorinterface, enabling demonstration recording, leader-follower control, and policy training with a wide range of input devices.Architecture
How the Two Systems Map
TeleopSessionTeleoperatorTeleopSession.step()→RetargeterIOTeleoperator.get_action()→RobotActionTeleopSessionConfig+ pipeline graphTeleoperatorConfigdataclassBaseRetargeterpipelineProcessorSteppipeline (optional)OptionalTensorGroupdict[str, float | np.ndarray]connect()/disconnect()Data Flow
block columns 1 block:lerobot["LeRobot"] columns 5 F["Robot.send_action()"] space:1 E["Record loop / control pipeline<br/>ProcessorStep (EE → joints IK)"] space:1 D["LeTeleop Teleoperator<br/>.get_action() → RobotAction"] end space:1 block:nvidia["NVIDIA Isaac Teleop"] columns 5 A["Device Interface<br/>(VR headset, controllers,<br/>hand tracking, manus gloves)"] space:1 B["Retargeting Pipeline<br/>(Se3, DexHand, Gripper,<br/>Locomotion)"] space:1 C["TeleopSession.step() →<br/>Dict[str, OptionalTensorGroup]"] end A --> B B --> C C --> D D --> E E --> F style A fill:#76b900,color:#fff style B fill:#76b900,color:#fff style C fill:#76b900,color:#fff style D fill:#ff9d00,color:#fff style E fill:#ff9d00,color:#fff style F fill:#ff9d00,color:#fff style nvidia fill:none,stroke:#76b900,stroke-width:2px style lerobot fill:none,stroke:#ff9d00,stroke-width:2pxComponents of Isaac Teleop
For LeRobot developers unfamiliar with Isaac Teleop, here is a quick tour of its layered architecture. Think of it as a pipeline: hardware sources produce raw tracking data, retargeters transform that data into robot-meaningful commands, and the session orchestrates the whole loop.
Sources (hardware → tensors)
Sources are stateless converters that poll hardware trackers and produce
OptionalTensorGroupoutputs. The built-in sources use OpenXR, but the architecture is not XR-only — plugins provide additional source types (e.g., foot pedals via Linux joystick, Manus gloves via Manus SDK). Each source auto-registers its tracker; you don't manage hardware directly.HandsSource(26,3), orientations(26,4)ControllersSource(3,), orientation(4,), trigger/squeeze/thumbstick scalarsHeadSource(3,), orientation(4,)FullBodySource(N,3), orientations(N,4), validity flagsGeneric3AxisPedalSource[-1, 1]All outputs are
OptionalTensorGroup— they have an.is_noneproperty that isTruewhen the input is unavailable (hand occluded, controller off, pedal disconnected, etc.). This is the Isaac Teleop equivalent of returningNone.Retargeters (tensors → robot commands)
Retargeters are pure-Python transform nodes that implement
BaseRetargeter. Each declares itsinput_spec()andoutput_spec(), then implements_compute_fn(). They are wired together via.connect()to form a DAG.Se3AbsRetargeterSe3RelRetargeterGripperRetargeter0.0–1.0DexHandRetargeterLocomotionRootCmdRetargeterTensorReordererLeRobot analogy: Retargeters are similar to LeRobot's
ProcessorStep— both are chainable transforms. The difference is that Isaac Teleop retargeters run inside the teleop session (beforeget_action()), while LeRobot processors run after (betweenget_action()andsend_action()).OutputCombiner (merging outputs)
OutputCombinertakes named outputs from multiple sources and retargeters and merges them into a single dict. This is the "finalization" step that defines whatTeleopSession.step()returns.TeleopSession (the orchestrator)
TeleopSessionis the top-level context manager that ties everything together. On construction, it auto-discovers which sources and trackers the pipeline needs. Each call to.step():Dict[str, OptionalTensorGroup]LeRobot analogy:
TeleopSessionis conceptually the "inner loop" that our_IsaacTeleopBase.get_action()wraps. LeRobot developers never interact withTeleopSessiondirectly — theIsaacTeleopController/IsaacTeleopHandsubclasses handle it.Plugins (optional hardware extensions)
Isaac Teleop supports plugins for devices beyond the built-in OpenXR sources:
Where Retargeting Happens
Isaac Teleop has its own retargeting engine (raw tracking → robot commands). LeRobot has a
ProcessorSteppipeline (action transforms, IK/FK). Two strategies:Isaac-side retargeting (recommended for dexterous hands, locomotion): Configure Isaac Teleop's pipeline with appropriate retargeters (
DexHandRetargeter,Se3AbsRetargeter,GripperRetargeter, etc.) so thatTeleopSession.step()already outputs robot-ready joint positions. The LeRobot teleoperator simply maps these toRobotActionkeys.LeRobot-side retargeting (recommended for simple EE control of arms): Pass through raw SE3 poses from Isaac Teleop, then use LeRobot's existing
EEReferenceAndDelta+InverseKinematicsEEToJointsprocessor steps to convert to joint targets for a specific follower robot.Both approaches can coexist; the config determines which retargeters are in the Isaac pipeline.
Scope
In Scope (Phase 1: Core Integration)
_IsaacTeleopBase: session lifecycle, plugin management, connect/disconnectIsaacTeleopController("isaac_teleop_controller"): VR controller aim pose + trigger/squeeze as gripperIsaacTeleopHand("isaac_teleop_hand"): Wrist pose + finger joint positions from hand tracking (OpenXR or Manus gloves)TeleoperatorConfigsubclass per semantic typeControllersSource/HandsSource→ optional retargeters →OutputCombinerOptionalTensorGrouptensors to LeRobotRobotActiondictsIn Scope (Phase 2: Advanced Features)
IsaacTeleopFullBody("isaac_teleop_full_body"): Body joint positions for humanoid teleoperationBiIsaacTeleopController("bi_isaac_teleop_controller"): Bimanual dual-controller setup (composition pattern)BiIsaacTeleopHand("bi_isaac_teleop_hand"): Bimanual hand trackingDexHandRetargeter,Se3AbsRetargeter,GripperRetargeter,TriHandMotionControllerRetargeteras config-level options within existing subclassessend_feedback()for haptic feedback via controllers (if supported by the device)Out of Scope
Teleoperator Subclassing Strategy
The Design Tension
Isaac Teleop's
TeleopSessionis fundamentally config-driven: one class handles all modes. The pipeline graph (sources + retargeters +OutputCombiner) determines both what hardware is active and what data flows out. Isaac Lab follows this pattern exactly — oneIsaacTeleopDeviceclass, with apipeline_buildercallable in config that returns anOutputCombiner. Different robots and control modes are entirely config-level variations.LeRobot's convention is different. Studying existing
Teleoperatorsubclasses reveals a clear pattern for when subclasses are created:IOSPhonevsAndroidPhone(HEBI SDK vs WebXR)KeyboardTeleopvsKeyboardEndEffectorTeleopvsKeyboardRoverTeleopaction_featuresreturns fundamentally different keys and typesBiSOLeaderwraps twoSOLeaderinstancesleft_/right_And when config alone suffices:
GamepadTeleopConfig.use_gripperKeyboardRoverTeleopConfig.linear_speedSO100Leader = SO101Leader = SOLeader(aliases)Reachy2TeleoperatorConfig.with_l_arm,.with_r_arm,.with_neckRecommended Approach: Subclass by Semantic Output
Isaac Teleop modes produce fundamentally different output semantics — a controller returns SE3 pose + trigger, hand tracking returns 26 joint poses, full-body returns a skeleton, a foot pedal returns velocity scalars. These map to different
action_featuresshapes. Following LeRobot convention, each semantic output type warrants its own subclass.However, within a semantic type, pipeline configuration (which retargeters to use, which robot's URDF) is handled by config — not subclassing.
classDiagram class _IsaacTeleopBase { <<abstract>> session lifecycle connect / disconnect plugin management +_build_pipeline()* OutputCombiner } class IsaacTeleopController { +action_features ee_pos : ndarray shape 3 ee_quat : ndarray shape 4 gripper : float --- Config: hand_side, gripper_source, retargeter (se3) } class IsaacTeleopHand { +action_features wrist_pos : ndarray shape 3 wrist_quat : ndarray shape 4 joint_positions : ndarray shape 26x3 --- Config: hand_side, retargeter (dex), retargeter_config } class IsaacTeleopFullBody { +action_features joint_positions : ndarray shape Nx3 joint_orientations : ndarray shape Nx4 joint_valid : ndarray shape N --- Config: (none yet) } _IsaacTeleopBase <|-- IsaacTeleopController _IsaacTeleopBase <|-- IsaacTeleopHand _IsaacTeleopBase <|-- IsaacTeleopFullBodyBimanual variants follow LeRobot's composition pattern (like
BiSOLeader):Why Not a Single Class with a
modeString?A single
IsaacTeleopclass withmode: str = "controller"would work but conflicts with LeRobot conventions:action_featureswould be mode-dependent — a consumer can't know the output shape without also knowing the mode. In LeRobot,action_featuresis a static contract per class.@TeleoperatorConfig.register_subclass("type_name")for CLI dispatch (--teleop.type=isaac_teleop_controller). Separate types are more discoverable.Why Not One Subclass Per Pipeline?
Going the other direction — one subclass per pipeline preset (e.g.,
IsaacTeleopDexHand,IsaacTeleopSe3Controller) — would be too granular. The retargeting choice changes the resolution of the output, not its semantic type. A controller withSe3AbsRetargeterstill outputs{ee_pos, ee_quat, gripper}— the retargeter just transforms the coordinates. This matches the Reachy2 pattern: one class with config-driven internal variation.Detailed Design
Package Structure
Configuration
Each semantic type gets its own config, registered separately for CLI discoverability:
Core Implementation
Shared base (not registered, not directly instantiable):
Controller teleoperator:
Bimanual controller (composition pattern matching
BiSOLeader):Tensor-to-Action Conversion
The core bridge logic converts Isaac Teleop's
OptionalTensorGroupoutput to LeRobot's flatRobotActiondict:For retargeted outputs (e.g., dexterous hand joints), the conversion reads the retargeter's named output tensors and maps them to joint-named keys matching the target robot.
Pipeline Builders
Each subclass's
_build_pipeline()creates an Isaac Teleop pipeline graph internally. Thepipelines.pymodule provides builder functions:action_featuresIsaacTeleopControllerControllersSourceSe3AbsRetargeter,Se3RelRetargeteree_pos (3,),ee_quat (4,),gripperIsaacTeleopHandHandsSourceDexHandRetargeterwrist_pos (3,),wrist_quat (4,),joint_positions (26,3)IsaacTeleopFullBodyFullBodySourcejoint_positions (N,3),joint_orientations (N,4),joint_valid (N,)BiIsaacTeleopControllerControllersSource(L+R)Se3AbsRetargeterleft_ee_pos,left_ee_quat,left_gripper,right_*BiIsaacTeleopHandHandsSource(L+R)DexHandRetargeterleft_wrist_pos,left_joint_positions,right_*Retargeters change the coordinate frame or resolution of the output, not its semantic type. A controller with
Se3AbsRetargeterstill outputs{ee_pos, ee_quat, gripper}— the retargeter maps poses from XR-space to robot-space.Usage Examples
Recording demonstrations with VR controller:
python -m lerobot.scripts.lerobot_record \ --robot.type=so100_follower \ --robot.port=/dev/ttyUSB0 \ --teleop.type=isaac_teleop_controller \ --teleop.hand_side=right \ --teleop.gripper_source=triggerBimanual hand tracking with dexterous retargeting:
python -m lerobot.scripts.lerobot_record \ --robot.type=custom_bimanual \ --teleop.type=bi_isaac_teleop_hand \ --teleop.retargeter=dex_hand \ --teleop.retargeter_config_path=./config/my_hand.yamlSimple teleoperation script:
Dependencies
Python Version Compatibility
This is a significant constraint. The only compatible Python version today is 3.12.
typestatement,T | Nonein non-annotation contexts) and pinsrequires-python = ">=3.12".Action required: Pin the integration to Python 3.12. Longer term, either extend Isaac Teleop's build matrix to include 3.13, or accept 3.12 as the single supported version for this integration.
Required
isaac_teleopPython package (built from/code/Teleop/via CMake → wheel)numpy >= 2.0Optional
scipy— forSe3RelRetargeterdex_retargeting— forDexHandRetargetertorch— if using dex_retargeting with optimizationBuild Requirement
Isaac Teleop must be built and its Python wheel installed:
Key Risks and Mitigations
OptionalTensorGroup.is_noneduring occlusiontracking_validflag; let downstream decide hold/stop behaviortensor_conversion.pyfor easy adaptationTesting Strategy
get_action()returns correct shapes and values.MockTeleoppattern to simulate Isaac Teleop outputs.IsaacTeleop.get_action()→LeRobotDataset.add_frame().Implementation Plan
Phase 1: Core Integration (MVP)
lerobot/teleoperators/isaac_teleop/with_base.py,controller.py,hand.py,config.py,pipelines.py,tensor_conversion.py_IsaacTeleopBase: Shared session lifecycle, connect/disconnect, plugin managementIsaacTeleopController: VR controller →{ee_pos, ee_quat, gripper}outputIsaacTeleopHand: Hand tracking →{wrist_pos, wrist_quat, joint_positions}output"isaac_teleop_controller"and"isaac_teleop_hand"tomake_teleoperator_from_config()lerobot_recordscriptPhase 2: Advanced Modes
BiIsaacTeleopController: Composition of left + right controllers (mirrorsBiSOLeaderpattern)BiIsaacTeleopHand: Composition of left + right hand trackingIsaacTeleopFullBody: Body joint output for humanoid teleoperationDexHandRetargeter,Se3AbsRetargeter,GripperRetargeteras config-level optionsPhase 3: Polish & Scale
send_feedback()References
docs/source/overview/architecture.rstexamples/lerobot/record.pyexamples/teleop_ros2/python/teleop_ros2_publisher.pyOutputCombiner(pipeline graph composition)TensorReorderer(tensor flattening/reordering for custom robots)ControllerInputIndex,HandInputIndex,FullBodyInputIndexTeleoperatorteleop_phone.pylerobot_record.py