mirror of
https://github.com/huggingface/lerobot.git
synced 2026-07-23 09:46:00 +00:00
293a8d9a77
* Add Isaac Teleop SO-101 leader-arm teleoperator
Add the NVIDIA Isaac Teleop teleoperator scaffolding and its first device:
SO101LeaderArm, a back-drivable SO-101 leader arm on Isaac Teleop's generic
joint-space device path. It reads the leader's joints from the so101_leader
plugin via JointStateSource and emits follower-ready {joint}.pos (rad2deg arm,
gripper -> RANGE_0_100) for direct 1:1 joint drive.
- IsaacTeleopTeleoperator base + IsaacTeleopConfig (shared session/CloudXR config)
- SO101LeaderArm / SO101LeaderArmConfig and leader_joints_to_robot_action
- examples/isaac_teleop_to_so101/teleoperate_leader.py example
- pure-numpy conversion tests
- isaac-teleop optional extra + NVIDIA PyPI index in pyproject
* Add Isaac Teleop XR controller teleoperator for SO-101
Add end-to-end XR (VR) controller teleoperation of an SO-101 follower arm via
the NVIDIA Isaac Teleop stack, layered on the Isaac Teleop scaffolding.
Teleoperator (src/lerobot/teleoperators/isaac_teleop/):
- XRController / XRControllerConfig: connect to the CloudXR runtime, auto-launch
the Isaac Teleop session, and expose get_action() emitting the raw base-frame
grip pose, squeeze, and trigger.
- MapXRControllerActionToRobotAction: stateless per-frame mapper from the XR
action to the IK input contract (absolute ee.x/y/z, ee.gripper_pos, wrist_roll).
- OverwriteWristRollFromAngle: post-IK step writing the operator wrist-roll [rad]
onto wrist_roll.pos [deg], recovering the under-determined roll DOF.
Example (examples/isaac_teleop_to_so101/):
- teleoperate.py: thin absolute-pose IK pipeline with an in-loop clutch (engage
latch + 1:1 delta rebase of position and orientation), EEBoundsAndSafety, and
InverseKinematicsEEToJoints; slews to a recorded home on startup.
- record_reset_pose.py / download_assets.py / webxr.env / .gitignore.
Also:
- Extend robot_kinematic_processor.py with EEBoundsAndSafety and
InverseKinematicsEEToJoints.
- Add XRControllerConfig + base_T_anchor to the Isaac Teleop config.
- Add docs/source/isaac_teleop.mdx and the _toctree entry.
- Add unit tests for the CloudXR launcher and the XR controller processor.
* Unify Isaac Teleop SO-101 scripts behind a mandatory device selector
Merge teleoperate.py (XR controller: clutch + soft-orientation IK) and
teleoperate_leader.py (SO-101 leader arm: 1:1 joint mirror) into a single
teleoperate.py driven by a `lerobot-teleoperate`-style draccus CLI: a follower
`--robot.*` and an input `--teleop.*`, where `--teleop.type` (xr_controller |
so101_leader) selects the Isaac device.
Uses a "dispatch, don't merge" shape: per-device setup_xr/setup_leader build a
Device bundle (compute / startup / cleanup / command); a shared slew() takes a
per-step target callable (XR a fixed reset pose, leader a live re-read so the
1:1 handoff stays continuous); one device-branchless outer loop runs both, with
compute() -> None meaning "hold at the measured pose" (XR disengaged or leader
stale). The entrypoint is @parser.wrap()'d over a TeleoperateConfig dataclass and
dispatches on the parsed config type; device knobs ride on --teleop.* (the leader
serial port is --teleop.port, forwarded to the plugin) and loop/launch knobs are
top-level (--launch_plugin=<path> collapses the old --launch-plugin/--plugin-bin
pair; --reset_to_origin/--align/--dry_run).
To let the Isaac devices claim the natural --teleop.type names without colliding
with the serial so101_leader of lerobot-teleoperate, give IsaacTeleopConfig its
own draccus choice registry (own _choice_registry, decoupled from the global
TeleoperatorConfig one) and register XRControllerConfig as "xr_controller" and
SO101LeaderArmConfig as "so101_leader" there; the example types its teleop field
as IsaacTeleopConfig so the choices resolve against that scoped registry. These
devices drive the bespoke clutch/IK/align loop and are not routed through
make_teleoperator_from_config, so dropping them from the global registry is inert.
YAGNI sweep of the commit train: delete the orphaned OverwriteWristRollFromAngle
(wrist_roll_processor.py) plus its export and tests -- no producer emits
wrist_roll; the live XR path uses orientation-weight IK on the 5-DOF arm by
design. Kept the load-bearing knobs (orientation_weight, raise_on_jump,
base_T_anchor) and the optional reset-pose recorder. Updated isaac_teleop.mdx
for the unified entrypoint and excised the stale roll-retargeter prose.
Net LOC down (two scripts 714 lines -> one), in-loop device branches reduced to
zero. Planned and reviewed via a 6-persona multi-agent panel (3-round planning
convergence + 2-round review). Verification (isaacteleop/placo not installable
here, so the device classes cannot connect, but their config dataclasses and the
script import fine via deferred imports): the teleoperators test suite passes
(45 passed, 2 skipped), draccus parsing of both target command lines yields the
right config subclass with scoped --teleop.type, --help renders the scoped
choices, the serial so101_leader stays in the global registry, and ruff
check/format are green.
Signed-off-by: Jiwen Cai <jiwenc@nvidia.com>
* Add Isaac Teleop SO-101 dataset recording script
record.py records a LeRobot dataset while driving the SO-101 follower
with either Isaac Teleop device (--teleop.type=xr_controller |
so101_leader), mirroring teleoperate.py's device dispatch.
* Extract shared Isaac Teleop SO-101 example infra into common.py
teleoperate.py and record.py both built the per-device pipeline and ran the
same read -> compute -> hold-when-idle -> sleep loop, with record.py importing
internals from teleoperate.py. Move the shared device/loop infrastructure
(Device, slew, Clutch, setup_xr/setup_leader + leader helpers, reset infra and
constants) into a new common.py, and add build_device() + hold_action() to
collapse the connect/dispatch/startup and idle-hold glue duplicated in both
entry points. The setup functions now type their config against a LoopConfig
Protocol, so common.py is decoupled from either CLI; both import from it.
Also rename record_reset_pose.py -> override_reset_pose.py so it is not confused
with record.py, and update the doc references.
* Add stdin keyboard backend so recording shortcuts work over SSH/headless
lerobot's init_keyboard_listener() uses pynput, which hooks GLOBAL key events
from the display server. Over SSH, under Wayland, or on a headless box with only a
TTY, keystrokes go to the terminal's stdin instead, so the listener never fires and
the Right/Left/Esc recording shortcuts silently do nothing.
Add a stdin (termios) keyboard backend to the example's common.py and an
init_keyboard_listener() that prefers it whenever stdin is an interactive TTY
(works over SSH / Wayland / headless-with-tty), falling back to lerobot's
pynput/headless listener for GUI launches with no controlling terminal. Selectable
via LEROBOT_KEYBOARD_BACKEND={auto,stdin,pynput,none}. The backend keeps ISIG so
Ctrl-C still works and always restores the terminal (on stop() and via atexit).
record.py now sources init_keyboard_listener from common; the Right/Left/Esc -> flag
mapping and the (listener, events) contract are unchanged.
Also convert record.py's loop_kwargs to a dict literal (ruff C408).
* Wait for the XR headset to connect before driving the arm
On the xr_controller path the example connected CloudXR and immediately ran the
reset slew + control loop, even if no headset was connected — the arm moved before
the operator was in VR, and get_action() just returned zeros so the clutch never
engaged.
Add an is_tracking property to XRController (set from the controller stream's
optional group, mirroring SO101LeaderArm) and a _wait_for_xr_controller() helper in
common.py that prints connection instructions (CloudXR web client URL + this
workstation's candidate IPv4s, with loopback/link-local and virtual/bridge/USB-gadget
interfaces filtered out) and polls until the controllers stream (indefinite, 15s
reminder, Ctrl-C to abort). setup_xr.startup() now connects, waits for the headset,
THEN runs the reset slew and seeds the clutch — so the arm only moves once the
operator is connected and watching. Mirrors the leader path's _wait_for_leader; both
record.py and teleoperate.py inherit it via the shared setup_xr.
* Address review feedback on the Isaac Teleop -> SO-101 example
Review-response and CI fixes for the Isaac Teleop -> SO-101 example.
- Move the XR Clutch into src/lerobot/teleoperators/isaac_teleop/clutch.py
(pure numpy + Rotation, no isaacteleop import), export it, and add
tests/teleoperators/test_clutch.py.
- Drop the vendored stdin keyboard listener; record.py uses a small terminal-
first wrapper over upstream's TerminalKeyListener (works over SSH even with a
local X display), falling back to upstream init_keyboard_listener otherwise.
- record.py: pass rgb_encoder/depth_encoder to LeRobotDataset create()/resume()
(upstream removed camera_encoder), fixing the AttributeError at record time.
- build_device: derive motor names from robot.action_features instead of
robot.bus (supports non-bus robots), and disconnect the follower if any step
after connect() fails so a failed setup never leaks the connection.
- Read leader joints by the group's declared names (_joints_group_to_rad)
instead of positionally, so a layout mismatch can't silently mirror the wrong
DOF onto the follower; add tests including a reversed-layout group.
- base.py: hoist `from pathlib import Path` to module scope; only the
isaacteleop CloudXRLauncher import stays lazy (optional dep).
- Trim the common.py module docstring and point to docs/source/isaac_teleop.mdx.
- default.env: correct the NV_DEVICE_PROFILE comment (auto-webrtc is the default;
this file overrides to Quest3, which works for both Quest 3 and Pico 4).
- download_assets.py: correct the RAW_BASE comment (tracks main, not pinned) and
add `# nosec B310` next to the existing `# noqa: S310` for the bandit hook.
- uv.lock: add the isaac-teleop extra's deps so `uv sync --locked` matches
pyproject; regenerated with uv 0.8.0 to keep lockfile revision 2 (CI's uv).
- isaac_teleop.mdx: prettier formatting.
* fix(.gitignore): removing .gitignore and using lerobot cache folder instead to store local user files
* chore(docstrings): reducing docstrings in default.env
* feat(URDF): cleaning up and simplifying the URDF download procedure
* feat(robot guard): adding a guard in case an unsupported robot type is provided (so-arms only)
* fix(imports): enforcing a python module structure to simplify imports
* feat(safe read): extending the motor bus safe read rationale to reset pose setting
* chore(trim): trimming lenghty comments and docstrings
* fix(deps): use isaacteleop [retargeters-lite] extra to unblock aarch64 (DGX Spark) (#3933)
* fix(deps): drop isaacteleop [retargeters] extra to unblock aarch64
The [retargeters] extra pulls dex-retargeting (pins numpy<2.0, conflicting
with lerobot's numpy>=2.0) and nlopt>=2.8 (no aarch64 wheels), making
lerobot[isaac-teleop] unresolvable on ARM (DGX Spark, Jetson Thor, GH200)
and over-constrained on numpy everywhere else.
The LeRobot teleoperators only import isaacteleop.retargeting_engine,
isaacteleop.cloudxr and isaacteleop.teleop_session_manager, all shipped in
the base wheel (requires only numpy>=1.23), so the extra is unused.
Verified on DGX Spark (aarch64, Python 3.12): resolves and installs with
isaacteleop 1.3.131 + numpy 2.2.6; all imported symbols load.
* fix(deps): use isaacteleop [retargeters-lite] extra for aarch64 support
Pin to isaacteleop ~=1.3.131 (the release that added ARM64/aarch64 support)
and swap the full [retargeters] extra for the new [retargeters-lite] one
(scipy-only). The full extra drags in dex-retargeting (pins numpy<2,
conflicting with lerobot's numpy>=2.0) and nlopt>=2.8 (no aarch64 wheels),
making lerobot[isaac-teleop] unresolvable on ARM hosts (DGX Spark, Jetson
Thor, GH200) and over-constrained on numpy everywhere else.
The LeRobot teleoperators only import isaacteleop.retargeting_engine,
isaacteleop.cloudxr and isaacteleop.teleop_session_manager — all covered
by the base wheel + retargeters-lite.
Verified on DGX Spark (aarch64, Python 3.12/3.13): resolves and installs
with isaacteleop 1.3.131 + numpy 2.2.6 + scipy 1.18.
* feat(deps): re-add full [retargeters] extra gated to x86_64
Keep the dex-retargeting/nlopt-based retargeters available on x86_64 (where
their wheels exist) via an environment marker, while ARM hosts (DGX Spark,
Jetson Thor, GH200) resolve with base + [retargeters-lite] only.
Verified: uv lock resolves on both platforms; on aarch64 the compile
excludes nlopt/dex-retargeting, on x86_64 they are included.
---------
Co-authored-by: Johnny Nunez <22727137+johnnynunez@users.noreply.github.com>
* chore(docstrings): trimming latest docstrings
* chore(teleop): move isaac-teleop to examples + update docs + add readme with installation notes
* chore(deps): restore uv.lock
* fix(example: isaac teleop parsing config
* fix(examples): isaac atomic-gripper controller
* feat(Examples): isaac-teleop holdlatch
* chore(examples): some other minor improvements for isaac-teleop
* chore(examples): top-level imports isaac-teleop
* chore(Examples): address ai review isaac-teleop
---------
Signed-off-by: Jiwen Cai <jiwenc@nvidia.com>
Co-authored-by: Jiwen Cai <jiwenc@nvidia.com>
Co-authored-by: Johnny <johnnync13@gmail.com>
Co-authored-by: Johnny Nunez <22727137+johnnynunez@users.noreply.github.com>
Co-authored-by: Steven Palma <steven.palma@huggingface.co>
Co-authored-by: Steven Palma <imstevenpmwork@ieee.org>
283 lines
11 KiB
Python
283 lines
11 KiB
Python
#!/usr/bin/env python
|
|
|
|
# Copyright 2026 NVIDIA Corporation and The HuggingFace Inc. team. All rights reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
"""Shared base for NVIDIA Isaac Teleop-backed LeRobot teleoperators.
|
|
|
|
Isaac Teleop is a multi-modal framework: a single ``TeleopSession`` can be driven by
|
|
XR controllers, hand tracking, Manus gloves, etc. Each modality is a
|
|
:class:`Teleoperator` subclass in its own ``teleop_<device>.py``.
|
|
|
|
:class:`IsaacTeleopTeleoperator` owns what those devices share — the session
|
|
lifecycle, the per-step staleness/worker-health guard, and the no-op calibration
|
|
tracking devices need. A concrete device implements :meth:`_build_pipeline` (its
|
|
retargeting graph) and :meth:`get_action` (usually via :meth:`_step`).
|
|
|
|
``isaacteleop`` is an optional NVIDIA dependency (install instructions in the example's
|
|
``README.md``); its imports are guarded behind an availability check at module top, so this
|
|
module imports without it and constructing a device fails fast with install instructions.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import abc
|
|
import logging
|
|
import os
|
|
from collections.abc import Mapping
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
from lerobot.teleoperators.teleoperator import Teleoperator
|
|
from lerobot.utils.import_utils import is_package_available
|
|
|
|
from .config_isaac_teleop import IsaacTeleopConfig
|
|
|
|
_isaacteleop_available = is_package_available("isaacteleop")
|
|
|
|
if TYPE_CHECKING or _isaacteleop_available:
|
|
from isaacteleop.cloudxr import CloudXRLauncher
|
|
from isaacteleop.retargeting_engine.interface import (
|
|
ExecutionEvents,
|
|
ExecutionState,
|
|
GraphExecutable,
|
|
RetargeterIO,
|
|
)
|
|
from isaacteleop.teleop_session_manager import TeleopSession, TeleopSessionConfig
|
|
else:
|
|
CloudXRLauncher = None
|
|
ExecutionEvents = None
|
|
ExecutionState = None
|
|
GraphExecutable = None
|
|
RetargeterIO = None
|
|
TeleopSession = None
|
|
TeleopSessionConfig = None
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Gripper closedness [0, 1] -> SO-101 follower motor units [0, 100] (RANGE_0_100, 100 = OPEN).
|
|
# Shared by the XR processor and leader device, which invert via ``pos = (1 - c) * SCALE``.
|
|
_GRIPPER_MOTOR_SCALE = 100.0
|
|
|
|
|
|
def _require_isaacteleop() -> None:
|
|
"""Fail fast with install pointers when the optional ``isaacteleop`` package is missing."""
|
|
if not _isaacteleop_available:
|
|
raise ImportError(
|
|
"The 'isaacteleop' package is required for Isaac Teleop devices but is not "
|
|
"installed. See examples/isaac_teleop_to_so101/README.md for install instructions."
|
|
)
|
|
|
|
|
|
class IsaacTeleopTeleoperator(Teleoperator):
|
|
"""Abstract base for teleoperators backed by an Isaac Teleop ``TeleopSession``.
|
|
|
|
Owns the session lifecycle and the per-step health guard; subclasses supply
|
|
:meth:`_build_pipeline` and :meth:`get_action`.
|
|
"""
|
|
|
|
config_class = IsaacTeleopConfig
|
|
|
|
def __init__(self, config: IsaacTeleopConfig):
|
|
_require_isaacteleop()
|
|
super().__init__(config)
|
|
self.config: IsaacTeleopConfig = config
|
|
self._session: TeleopSession | None = None
|
|
self._cloudxr_launcher: CloudXRLauncher | None = None
|
|
|
|
# ------------------------------------------------------------------
|
|
# Pipeline construction (device override point)
|
|
# ------------------------------------------------------------------
|
|
|
|
@abc.abstractmethod
|
|
def _build_pipeline(self) -> GraphExecutable:
|
|
"""Build this device's retargeting pipeline (the ``GraphExecutable`` for
|
|
``TeleopSessionConfig.pipeline``). Called once in :meth:`connect`; its output
|
|
keys must match what :meth:`get_action` unpacks.
|
|
"""
|
|
raise NotImplementedError
|
|
|
|
# ------------------------------------------------------------------
|
|
# Lifecycle (shared)
|
|
# ------------------------------------------------------------------
|
|
|
|
@property
|
|
def is_connected(self) -> bool:
|
|
return self._session is not None
|
|
|
|
@property
|
|
def is_calibrated(self) -> bool:
|
|
return True # Tracking devices are self-calibrating.
|
|
|
|
def calibrate(self) -> None:
|
|
pass
|
|
|
|
def configure(self) -> None:
|
|
pass
|
|
|
|
def connect(self, calibrate: bool = True) -> None:
|
|
"""Auto-launch the CloudXR runtime (unless opted out) and open the session.
|
|
|
|
The CloudXR launch blocks ~30s and, on the first run, prompts on stdin for the
|
|
EULA (accept once via ``python -m isaacteleop.cloudxr --accept-eula``). Opt out
|
|
when CloudXR runs externally via ``config.auto_launch_cloudxr=False`` or
|
|
``LEROBOT_CLOUDXR_SKIP_AUTOLAUNCH=1`` (env var wins).
|
|
"""
|
|
if self._session is not None:
|
|
raise RuntimeError("Already connected. Call disconnect() first.")
|
|
|
|
self._ensure_cloudxr_runtime()
|
|
|
|
try:
|
|
pipeline = self._build_pipeline()
|
|
session_config = TeleopSessionConfig(app_name=self.config.app_name, pipeline=pipeline)
|
|
self._session = TeleopSession(session_config)
|
|
self._session.__enter__()
|
|
except Exception:
|
|
self._session = None
|
|
try:
|
|
self._stop_cloudxr_runtime()
|
|
except Exception:
|
|
logger.exception("Failed to stop CloudXR runtime during connect() rollback")
|
|
raise
|
|
logger.info("Isaac Teleop session started: %s", self.config.app_name)
|
|
|
|
def disconnect(self) -> None:
|
|
try:
|
|
if self._session is not None:
|
|
# Null the handle BEFORE __exit__: even a failed session teardown must not
|
|
# wedge the device as is_connected (blocking every later connect/disconnect).
|
|
session = self._session
|
|
self._session = None
|
|
session.__exit__(None, None, None)
|
|
logger.info("Isaac Teleop session ended")
|
|
finally:
|
|
# Reap the CloudXR runtime even if session teardown raised, and even if no
|
|
# session was ever established (e.g. the launcher came up but session creation
|
|
# failed before this point); a no-op when we never launched CloudXR (opt-out /
|
|
# externally-owned runtime), so we never stop a runtime we don't own.
|
|
self._stop_cloudxr_runtime()
|
|
|
|
# ------------------------------------------------------------------
|
|
# CloudXR runtime (shared)
|
|
# ------------------------------------------------------------------
|
|
|
|
def _ensure_cloudxr_runtime(self) -> None:
|
|
"""Auto-launch the CloudXR runtime once, unless opted out.
|
|
|
|
Idempotent (no-op once the launcher is up). ``LEROBOT_CLOUDXR_SKIP_AUTOLAUNCH``
|
|
is checked first and wins over ``config.auto_launch_cloudxr``. Constructing
|
|
:class:`CloudXRLauncher` mutates the process env (``XR_RUNTIME_JSON`` etc.) and
|
|
blocks until the runtime is ready or raises :class:`RuntimeError`.
|
|
"""
|
|
if self._cloudxr_launcher is not None:
|
|
return
|
|
|
|
if os.environ.get("LEROBOT_CLOUDXR_SKIP_AUTOLAUNCH", "").strip() == "1":
|
|
logger.info(
|
|
"LEROBOT_CLOUDXR_SKIP_AUTOLAUNCH=1 set; skipping CloudXR auto-launch "
|
|
"(assuming CloudXR is already running externally)"
|
|
)
|
|
return
|
|
|
|
if not self.config.auto_launch_cloudxr:
|
|
logger.info(
|
|
"config.auto_launch_cloudxr is False; skipping CloudXR auto-launch "
|
|
"(assuming CloudXR is already running externally)"
|
|
)
|
|
return
|
|
|
|
logger.info("Launching CloudXR runtime (first run may prompt for EULA and take ~30s)...")
|
|
|
|
self._cloudxr_launcher = CloudXRLauncher(
|
|
install_dir=str(Path.home() / ".cloudxr"),
|
|
env_config=self.config.cloudxr_env_file,
|
|
accept_eula=False,
|
|
)
|
|
|
|
def _stop_cloudxr_runtime(self) -> None:
|
|
"""Stop the auto-launched CloudXR runtime, if any.
|
|
|
|
Clean stop nulls the handle. On :class:`RuntimeError` the handle is RETAINED so
|
|
the launcher's ``atexit`` hook owns the retry — a later :meth:`connect` then
|
|
treats the retained runtime as still up and will not relaunch.
|
|
"""
|
|
if self._cloudxr_launcher is None:
|
|
return
|
|
try:
|
|
self._cloudxr_launcher.stop()
|
|
except RuntimeError:
|
|
logger.warning("CloudXR runtime could not be terminated; handle retained for atexit cleanup")
|
|
else:
|
|
self._cloudxr_launcher = None
|
|
logger.info("CloudXR runtime stopped")
|
|
|
|
def send_feedback(self, feedback: dict[str, Any]) -> None:
|
|
pass # Haptic feedback not yet implemented.
|
|
|
|
# ------------------------------------------------------------------
|
|
# Stepping (shared)
|
|
# ------------------------------------------------------------------
|
|
|
|
def _running_events(self) -> ExecutionEvents:
|
|
"""Constant ``RUNNING`` ``ExecutionEvents`` for a device with no clutch lifecycle.
|
|
|
|
Keeps the stream flowing; ``reset`` stays ``False``. A clutched device that needs
|
|
a real lifecycle should build its own ``ExecutionEvents`` instead.
|
|
"""
|
|
return ExecutionEvents(execution_state=ExecutionState.RUNNING, reset=False)
|
|
|
|
def _step(
|
|
self,
|
|
*,
|
|
execution_events: ExecutionEvents | None = None,
|
|
external_inputs: Mapping[str, Any] | None = None,
|
|
) -> RetargeterIO:
|
|
"""Step the session once and return the raw pipeline outputs.
|
|
|
|
Applies the shared guard: re-raises a retargeting-worker exception and warns on a
|
|
stale frame. Subclasses call this from :meth:`get_action`.
|
|
|
|
Args:
|
|
execution_events: The ``ExecutionEvents`` driving the session this frame.
|
|
Devices with a lifecycle (clutch) MUST pass this every frame — when
|
|
``None``, ``TeleopSession.step`` auto-fires ``RUNNING`` (the clutch would
|
|
latch immediately and never stop).
|
|
external_inputs: Per-step inputs (e.g. a static ``base_T_anchor``) in the
|
|
``{leaf_node_name: {output_port_name: TensorGroup}}`` shape ``step`` expects.
|
|
|
|
Raises:
|
|
RuntimeError: If not connected, or if the retargeting worker raised.
|
|
"""
|
|
if self._session is None:
|
|
raise RuntimeError("Not connected. Call connect() first.")
|
|
|
|
result = self._session.step(
|
|
execution_events=execution_events,
|
|
external_inputs=external_inputs,
|
|
)
|
|
|
|
info = self._session.last_step_info
|
|
if info is not None:
|
|
if info.worker_exception is not None:
|
|
raise RuntimeError(
|
|
"Isaac Teleop retargeting worker raised an exception"
|
|
) from info.worker_exception
|
|
if info.frame_deadline_miss:
|
|
logger.warning(
|
|
"Isaac Teleop frame deadline miss (returned_age_frames=%s)",
|
|
info.returned_age_frames,
|
|
)
|
|
return result
|