diff --git a/docs/source/device/gamepad.rst b/docs/source/device/gamepad.rst index c1a615e3af..6e424547d9 100644 --- a/docs/source/device/gamepad.rst +++ b/docs/source/device/gamepad.rst @@ -100,7 +100,7 @@ Usage Start the plugin with the session through ``TeleopSessionConfig.plugins``, from the copy bundled with ``isaaccapture`` (wheel installs included), and read it with ``GamepadSource``, as -:code-file:`examples/teleop/python/gamepad_printer_example.py` does: +:code-file:`examples/teleop/python/isaaccapture_examples/teleop/gamepad_printer_example.py` does: .. code-block:: python diff --git a/docs/source/device/spatial_controllers/spacemouse.rst b/docs/source/device/spatial_controllers/spacemouse.rst index 9d896bccfb..84438113c0 100644 --- a/docs/source/device/spatial_controllers/spacemouse.rst +++ b/docs/source/device/spatial_controllers/spacemouse.rst @@ -66,7 +66,7 @@ Usage Start the plugin with the session through ``TeleopSessionConfig.plugins``, from the copy bundled with ``isaaccapture`` (wheel installs included), and read it with ``SpaceMouseSource``, as -:code-file:`examples/teleop/python/spacemouse_printer_example.py` does: +:code-file:`examples/teleop/python/isaaccapture_examples/teleop/spacemouse_printer_example.py` does: .. code-block:: python diff --git a/docs/source/device/trackers/index.rst b/docs/source/device/trackers/index.rst index bee082b049..09d68b2a01 100644 --- a/docs/source/device/trackers/index.rst +++ b/docs/source/device/trackers/index.rst @@ -252,8 +252,8 @@ axis inputs. Uses standard OpenXR action bindings. - Examples: - :code-file:`examples/retargeting/python/isaaccapture_examples/retargeting/sources_example.py` - - :code-file:`examples/teleop/python/locomotion_retargeting_example.py` - - :code-file:`examples/teleop/python/gripper_retargeting_example_simple.py` + - :code-file:`examples/teleop/python/isaaccapture_examples/teleop/locomotion_retargeting_example.py` + - :code-file:`examples/teleop/python/isaaccapture_examples/teleop/gripper_retargeting_example_simple.py` FullBodyTracker ~~~~~~~~~~~~~~~ @@ -434,7 +434,7 @@ utility internally. - Examples: - :code-file:`examples/schemaio/pedal_printer.cpp` - - :code-file:`examples/teleop/python/foot_pedal_locomotion_example.py` + - :code-file:`examples/teleop/python/isaaccapture_examples/teleop/foot_pedal_locomotion_example.py` .. note:: @@ -505,5 +505,5 @@ For end-to-end usage patterns combining trackers with a ``DeviceIOSession``, see For higher-level usage with the teleop session manager and retargeting, see: - :code-file:`examples/retargeting/python/isaaccapture_examples/retargeting/sources_example.py` -- :code-file:`examples/teleop/python/gripper_retargeting_example_simple.py` -- :code-file:`examples/teleop/python/locomotion_retargeting_example.py` +- :code-file:`examples/teleop/python/isaaccapture_examples/teleop/gripper_retargeting_example_simple.py` +- :code-file:`examples/teleop/python/isaaccapture_examples/teleop/locomotion_retargeting_example.py` diff --git a/docs/source/references/cloudxr.rst b/docs/source/references/cloudxr.rst index 0befeae243..28f45e4656 100644 --- a/docs/source/references/cloudxr.rst +++ b/docs/source/references/cloudxr.rst @@ -213,13 +213,15 @@ for themselves and need no sourcing. Run teleop examples against the service --------------------------------------- -Nothing to pass. The examples under ``examples/teleop/python/`` attach to -whatever runtime is serving, so with a service running they use it: +Nothing to pass. The examples under ``examples/teleop`` attach to +whatever runtime is serving, so with a service running they use it. Install +them once from the repository root, as in :ref:`run-teleop-example`: .. code-block:: bash + uv pip install -e ./examples/teleop python -m isaaccapture.cloudxr.service start --accept-eula - python examples/teleop/python/gripper_retargeting_example_simple.py + python -m isaaccapture_examples.teleop.gripper_retargeting_example_simple The example leaves the service running when it exits, so the headset stays connected and the next run reattaches to the same session. @@ -242,7 +244,7 @@ other than CloudXR: export XR_RUNTIME_JSON=/path/to/your/openxr.json export ISAAC_TELEOP_DISABLE_CXR_ENV_CHECKS=1 - python examples/teleop/python/gripper_retargeting_example_simple.py --no-launch-cloudxr-runtime + python -m isaaccapture_examples.teleop.gripper_retargeting_example_simple --no-launch-cloudxr-runtime Run the service in a container or CI ------------------------------------ diff --git a/docs/source/references/retargeting/index.rst b/docs/source/references/retargeting/index.rst index 712ba5ead5..935996d6a0 100644 --- a/docs/source/references/retargeting/index.rst +++ b/docs/source/references/retargeting/index.rst @@ -248,7 +248,7 @@ Available Retargeters For a complete hand retargeting example (e.g. ``HandsSource`` + ``DexHandRetargeter`` or ``TriHandMotionControllerRetargeter``, connect and compute), see ``g1_trihand_retargeting_example.py`` and ``dex_bimanual_example.py`` in the - :code-dir:`examples/teleop/python` directory. + :code-dir:`examples/teleop/python/isaaccapture_examples/teleop` directory. .. dropdown:: LocomotionRootCmdRetargeter diff --git a/docs/source/references/retargeting/joint_space.rst b/docs/source/references/retargeting/joint_space.rst index e64b37ad0f..503ad0aeb1 100644 --- a/docs/source/references/retargeting/joint_space.rst +++ b/docs/source/references/retargeting/joint_space.rst @@ -102,17 +102,19 @@ and flatten ``ee_pose`` + ``gripper_command`` into the env's task-space action l Run the example --------------- -The repo ships ``examples/teleop/python/joint_space_device_example.py``: +The repo ships ``examples/teleop/python/isaaccapture_examples/teleop/joint_space_device_example.py``: .. code-block:: console + $ uv pip install -e ./examples/teleop + # Consumes the so101_leader plugin over OpenXR (source cloudxr.env first): - $ python joint_space_device_example.py --launch-plugin --mode joint --frames 8 - $ python joint_space_device_example.py --launch-plugin --mode ee --urdf /path/to/so101_new_calib.urdf + $ python -m isaaccapture_examples.teleop.joint_space_device_example --launch-plugin --mode joint --frames 8 + $ python -m isaaccapture_examples.teleop.joint_space_device_example --launch-plugin --mode ee --urdf /path/to/so101_new_calib.urdf ``so101_new_calib.urdf`` is not shipped in this repo. It is ``Simulation/SO101/so101_new_calib.urdf`` in `TheRobotStudio/SO-ARM100 `_; pass its path to -``--urdf``. Run the commands from ``examples/teleop/python/``. +``--urdf``. Run the commands from the repository root. Validate -------- diff --git a/docs/source/teleop/quick_start.rst b/docs/source/teleop/quick_start.rst index db4afba394..46a37f6057 100644 --- a/docs/source/teleop/quick_start.rst +++ b/docs/source/teleop/quick_start.rst @@ -116,7 +116,7 @@ env file and start the service with it: python -m isaaccapture.cloudxr.service start --cloudxr-env-config ./custom.env The same ``--cloudxr-env-config`` flag is available on the teleop examples under -``examples/teleop/python/``, which register CloudXR's launcher arguments through +``examples/teleop``, which register CloudXR's launcher arguments through ``CloudXRLauncher.add_launcher_arguments()`` — but it applies only when the example is the one starting the runtime. With a service already running the example attaches to it instead, and prints which settings it had to ignore (see @@ -332,13 +332,15 @@ running the CloudXR runtime and wss proxy in containerized environment; or using 6. Run a teleop example ------------------------ -Run the simplified gripper retargeting example. This demonstrates the full +Install the teleop examples into the environment from step 2, then run the +simplified gripper retargeting example. This demonstrates the full pipeline: reading XR controller input via CloudXR, retargeting it through the ``GripperRetargeter``, and printing the resulting gripper command values: .. code-block:: bash - python examples/teleop/python/gripper_retargeting_example_simple.py + uv pip install -e ./examples/teleop + python -m isaaccapture_examples.teleop.gripper_retargeting_example_simple Once running, squeeze the **right controller trigger** to control the gripper. ``GripperRetargeter`` emits binary commands: ``+1.00`` means open and ``-1.00`` @@ -365,7 +367,7 @@ chosen duration, measured after session startup, pass ``--duration``: .. code-block:: bash - python examples/teleop/python/gripper_retargeting_example_simple.py --duration 20 + python -m isaaccapture_examples.teleop.gripper_retargeting_example_simple --duration 20 ``--duration 0`` also runs until Ctrl+C. Both a timed run and Ctrl+C announce that the session is closing, then print ``Session closed.`` after cleanup. @@ -377,12 +379,13 @@ For proportional controller input, read ``ControllerInputIndex.TRIGGER_VALUE`` or use ``SO101GripperRetargeter`` for SO-101 jaw closedness in ``[0, 1]`` (``0`` open, ``1`` closed); see :doc:`../references/retargeting/so101`. -To try other examples, see ``examples/teleop/python/`` — for instance: +To try other examples, run another module of ``isaaccapture_examples.teleop`` +the same way; :code-file:`examples/teleop/README.md` lists them all. For instance: -- ``se3_retargeting_example.py`` — maps hand or controller poses to +- ``se3_retargeting_example`` — maps hand or controller poses to end-effector poses (absolute or relative), with the same ``--duration`` and Ctrl+C behavior -- ``dex_bimanual_example.py`` — bimanual dexterous hand retargeting -- ``gripper_retargeting_example.py`` — full gripper example with more +- ``dex_bimanual_example`` — bimanual dexterous hand retargeting +- ``gripper_retargeting_example`` — full gripper example with more configuration options Next steps diff --git a/docs/source/teleop/teleop_session.rst b/docs/source/teleop/teleop_session.rst index a04ab50aa0..b48d411c47 100644 --- a/docs/source/teleop/teleop_session.rst +++ b/docs/source/teleop/teleop_session.rst @@ -456,14 +456,18 @@ Examples Complete Examples ^^^^^^^^^^^^^^^^^ -#. **Simplified Gripper Example**: ``examples/teleop/python/gripper_retargeting_example_simple.py`` +#. **Simplified Gripper Example**: ``examples/teleop/python/isaaccapture_examples/teleop/gripper_retargeting_example_simple.py`` -- Shows the minimal configuration approach and demonstrates auto-creation - of input sources. Run it from the project root, with the environment set up - as in the :doc:`Quick Start `: + of input sources. Install ``isaaccapture`` into the active environment first: + from your source build as in + :doc:`step 4 of Build from source `, or + from the package index as in the :doc:`Quick Start `. Then install + this example from the project root: .. code-block:: bash - python examples/teleop/python/gripper_retargeting_example_simple.py + uv pip install -e ./examples/teleop + python -m isaaccapture_examples.teleop.gripper_retargeting_example_simple Before vs After ^^^^^^^^^^^^^^^ diff --git a/examples/teleop/CMakeLists.txt b/examples/teleop/CMakeLists.txt deleted file mode 100644 index 94a93beb11..0000000000 --- a/examples/teleop/CMakeLists.txt +++ /dev/null @@ -1,8 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -cmake_minimum_required(VERSION 3.20) -project(teleop_examples) - -include(${CMAKE_SOURCE_DIR}/cmake/InstallPythonExample.cmake) -install_python_example(DESTINATION examples/teleop/python) diff --git a/examples/teleop/README.md b/examples/teleop/README.md new file mode 100644 index 0000000000..12364979d7 --- /dev/null +++ b/examples/teleop/README.md @@ -0,0 +1,55 @@ + + +# Teleop Examples + +Each example reads hands, controllers or a device through `TeleopSession` and +prints the result; most retarget it first. Each connects through CloudXR and +starts the runtime itself if none is serving. + +Install `isaaccapture` into the active environment first: from your source +build as in [Running an example](../README.md#running-an-example), or from the +package index as in the +[Quick Start](https://nvidia.github.io/IsaacCapture/main/teleop/quick_start.html). +Then install this example from the repository root: + +```bash +uv pip install -e ./examples/teleop +python -m isaaccapture_examples.teleop.gripper_retargeting_example_simple +``` + +Every module runs the same way, as +`python -m isaaccapture_examples.teleop.`; `--help` lists its options. + +| Module | What it shows | +| --- | --- | +| `gripper_retargeting_example_simple` | Controller trigger to gripper command, with the minimal `TeleopSession` setup. | +| `gripper_retargeting_example` | A hand-built gripper and end-effector graph combined into one action. | +| `isaac_lab_gripper_example` | Thumb-index pinch to gripper command (hand tracking). | +| `se3_retargeting_example` | Hand or controller pose to an absolute or relative end-effector pose. | +| `dex_bimanual_example` | Both hands to robot hand joints, with one `DexHandRetargeter` per hand. | +| `full_bimanual_reordering_example` | Arms and hands flattened into one Isaac Lab action tensor. | +| `g1_trihand_retargeting_example` | Motion controllers to G1 three-finger hand joints. | +| `locomotion_retargeting_example` | Controller sticks to robot base commands. | +| `foot_pedal_locomotion_example` | A 3-axis foot pedal to robot base commands. | +| `joint_space_device_example` | An SO-101 leader arm to an Isaac Lab action. | +| `gamepad_printer_example` | Gamepad axes and buttons, and what the gamepad retargeters make of them. | +| `spacemouse_printer_example` | SpaceMouse axes and buttons. | +| `keyboard_printer_example` | Keys from a small GLFW window. | +| `keyboard_terminal_example` | Keys from the terminal, with no window. | + +The bimanual examples default to the dex-retargeting YAML and URDF files in +[`config/dex_retargeting/`](python/isaaccapture_examples/teleop/config/dex_retargeting/), +which install with the package. Their `--enable-tuning` window needs ImGui from +the `ui` extra. Of the supported Pythons, `imgui` 2.0.0 has wheels only for +3.11: on 3.12 it builds from source and needs a C++ toolchain, and on 3.13 it +does not build at all: + +```bash +uv pip install -e './examples/teleop[ui]' +``` + +The retargeters these examples use are documented in +[Retargeting](https://nvidia.github.io/IsaacCapture/main/references/retargeting/index.html). diff --git a/examples/teleop/pyproject.toml b/examples/teleop/pyproject.toml new file mode 100644 index 0000000000..df1020cdff --- /dev/null +++ b/examples/teleop/pyproject.toml @@ -0,0 +1,32 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +# The dist name mirrors the import path, so an installed example claims no bare +# top-level name in site-packages. +name = "isaaccapture-examples-teleop" +version = "0.1.0" +description = "Isaac Teleop Retargeting Examples" +requires-python = ">=3.11,<3.14" +dependencies = [ + "isaaccapture[cloudxr,retargeters]", + "glfw>=2.7", # keyboard_printer_example: the window and its platform constants +] + +[project.optional-dependencies] +# The bimanual examples' --enable-tuning window. Optional because imgui does not +# install on every supported Python; see README.md. +ui = ["isaaccapture[ui]"] + +# `only-include` + `sources`, not `packages`, and no __init__.py above +# teleop -- see the packaging rules in examples/README.md. +# +# config/ lives inside the package because the bimanual examples resolve it +# from __file__, so the YAML and URDF must travel with them. +[tool.hatch.build.targets.wheel] +only-include = ["python/isaaccapture_examples/teleop"] +sources = ["python"] diff --git a/examples/teleop/python/isaaccapture_examples/teleop/__init__.py b/examples/teleop/python/isaaccapture_examples/teleop/__init__.py new file mode 100644 index 0000000000..52a7a9daf0 --- /dev/null +++ b/examples/teleop/python/isaaccapture_examples/teleop/__init__.py @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 diff --git a/examples/teleop/python/_example_loop.py b/examples/teleop/python/isaaccapture_examples/teleop/_example_loop.py similarity index 100% rename from examples/teleop/python/_example_loop.py rename to examples/teleop/python/isaaccapture_examples/teleop/_example_loop.py diff --git a/examples/teleop/python/config/dex_retargeting/README.md b/examples/teleop/python/isaaccapture_examples/teleop/config/dex_retargeting/README.md similarity index 100% rename from examples/teleop/python/config/dex_retargeting/README.md rename to examples/teleop/python/isaaccapture_examples/teleop/config/dex_retargeting/README.md diff --git a/examples/teleop/python/config/dex_retargeting/hand_left_config.yml b/examples/teleop/python/isaaccapture_examples/teleop/config/dex_retargeting/hand_left_config.yml similarity index 100% rename from examples/teleop/python/config/dex_retargeting/hand_left_config.yml rename to examples/teleop/python/isaaccapture_examples/teleop/config/dex_retargeting/hand_left_config.yml diff --git a/examples/teleop/python/config/dex_retargeting/hand_right_config.yml b/examples/teleop/python/isaaccapture_examples/teleop/config/dex_retargeting/hand_right_config.yml similarity index 100% rename from examples/teleop/python/config/dex_retargeting/hand_right_config.yml rename to examples/teleop/python/isaaccapture_examples/teleop/config/dex_retargeting/hand_right_config.yml diff --git a/examples/teleop/python/config/dex_retargeting/left_robot_hand.urdf b/examples/teleop/python/isaaccapture_examples/teleop/config/dex_retargeting/left_robot_hand.urdf similarity index 100% rename from examples/teleop/python/config/dex_retargeting/left_robot_hand.urdf rename to examples/teleop/python/isaaccapture_examples/teleop/config/dex_retargeting/left_robot_hand.urdf diff --git a/examples/teleop/python/config/dex_retargeting/right_robot_hand.urdf b/examples/teleop/python/isaaccapture_examples/teleop/config/dex_retargeting/right_robot_hand.urdf similarity index 100% rename from examples/teleop/python/config/dex_retargeting/right_robot_hand.urdf rename to examples/teleop/python/isaaccapture_examples/teleop/config/dex_retargeting/right_robot_hand.urdf diff --git a/examples/teleop/python/dex_bimanual_example.py b/examples/teleop/python/isaaccapture_examples/teleop/dex_bimanual_example.py similarity index 91% rename from examples/teleop/python/dex_bimanual_example.py rename to examples/teleop/python/isaaccapture_examples/teleop/dex_bimanual_example.py index 70b69c34ad..cbf5de5bd4 100644 --- a/examples/teleop/python/dex_bimanual_example.py +++ b/examples/teleop/python/isaaccapture_examples/teleop/dex_bimanual_example.py @@ -1,10 +1,10 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 """ Dex BiManual Retargeting Example -Demonstrates using DexBiManualRetargeter to control two hands simultaneously. +Drives both hands at once with one DexHandRetargeter per hand. """ import sys @@ -22,7 +22,6 @@ ) from isaaccapture.retargeting_engine.interface import OutputCombiner from isaaccapture.teleop_session_manager import TeleopSession, TeleopSessionConfig -from isaaccapture.retargeting_engine_ui import MultiRetargeterTuningUIImGui def main(): @@ -50,10 +49,21 @@ def main(): "--right-params", type=str, help="Path to right hand tunable parameters (JSON)" ) parser.add_argument( - "--enable-tuning", action="store_true", help="Enable retargeting tuning UI" + "--enable-tuning", + action="store_true", + help="Enable retargeting tuning UI (needs the ui extra)", ) CloudXRLauncher.add_launcher_arguments(parser) args = parser.parse_args() + # Before the retargeters load and CloudXR starts: imgui is the optional `ui` extra. + if args.enable_tuning: + try: + from isaaccapture.retargeting_engine_ui import MultiRetargeterTuningUIImGui + except ImportError: + parser.error( + "--enable-tuning needs the ui extra: " + "uv pip install -e './examples/teleop[ui]'" + ) # Check for config files # Note: These paths are placeholders. In a real scenario, you'd point to your robot's config. diff --git a/examples/teleop/python/foot_pedal_locomotion_example.py b/examples/teleop/python/isaaccapture_examples/teleop/foot_pedal_locomotion_example.py similarity index 94% rename from examples/teleop/python/foot_pedal_locomotion_example.py rename to examples/teleop/python/isaaccapture_examples/teleop/foot_pedal_locomotion_example.py index 08c533e514..77091b9c53 100644 --- a/examples/teleop/python/foot_pedal_locomotion_example.py +++ b/examples/teleop/python/isaaccapture_examples/teleop/foot_pedal_locomotion_example.py @@ -30,7 +30,9 @@ ) -PLUGIN_ROOT_DIR = Path(__file__).resolve().parent.parent.parent.parent / "plugins" +# Six levels up is the repo root in a checkout or editable install. It has no +# plugins/ directory, so this example runs without the synthetic-hands plugin. +PLUGIN_ROOT_DIR = Path(__file__).resolve().parents[5] / "plugins" PLUGIN_NAME = "controller_synthetic_hands" PLUGIN_ROOT_ID = "synthetic_hands" diff --git a/examples/teleop/python/full_bimanual_reordering_example.py b/examples/teleop/python/isaaccapture_examples/teleop/full_bimanual_reordering_example.py similarity index 94% rename from examples/teleop/python/full_bimanual_reordering_example.py rename to examples/teleop/python/isaaccapture_examples/teleop/full_bimanual_reordering_example.py index 9f1f49630b..c08334d2b1 100644 --- a/examples/teleop/python/full_bimanual_reordering_example.py +++ b/examples/teleop/python/isaaccapture_examples/teleop/full_bimanual_reordering_example.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 """ @@ -29,7 +29,6 @@ ) from isaaccapture.retargeting_engine.interface import OutputCombiner from isaaccapture.teleop_session_manager import TeleopSession, TeleopSessionConfig -from isaaccapture.retargeting_engine_ui import MultiRetargeterTuningUIImGui def main(): @@ -50,10 +49,21 @@ def main(): "--right-config", type=str, help="Path to right hand retargeting config (YAML)" ) parser.add_argument( - "--enable-tuning", action="store_true", help="Enable retargeting tuning UI" + "--enable-tuning", + action="store_true", + help="Enable retargeting tuning UI (needs the ui extra)", ) CloudXRLauncher.add_launcher_arguments(parser) args = parser.parse_args() + # Before the retargeters load and CloudXR starts: imgui is the optional `ui` extra. + if args.enable_tuning: + try: + from isaaccapture.retargeting_engine_ui import MultiRetargeterTuningUIImGui + except ImportError: + parser.error( + "--enable-tuning needs the ui extra: " + "uv pip install -e './examples/teleop[ui]'" + ) # Config paths (similar to dex_bimanual_example) config_dir = Path(__file__).parent / "config" / "dex_retargeting" diff --git a/examples/teleop/python/g1_trihand_retargeting_example.py b/examples/teleop/python/isaaccapture_examples/teleop/g1_trihand_retargeting_example.py similarity index 96% rename from examples/teleop/python/g1_trihand_retargeting_example.py rename to examples/teleop/python/isaaccapture_examples/teleop/g1_trihand_retargeting_example.py index 7e837923e5..2788e7ecb8 100644 --- a/examples/teleop/python/g1_trihand_retargeting_example.py +++ b/examples/teleop/python/isaaccapture_examples/teleop/g1_trihand_retargeting_example.py @@ -26,7 +26,9 @@ from isaaccapture.retargeting_engine.interface import OutputCombiner -PLUGIN_ROOT_DIR = Path(__file__).resolve().parent.parent.parent.parent / "plugins" +# Six levels up is the repo root in a checkout or editable install. It has no +# plugins/ directory, so this example runs without the synthetic-hands plugin. +PLUGIN_ROOT_DIR = Path(__file__).resolve().parents[5] / "plugins" PLUGIN_NAME = "controller_synthetic_hands" PLUGIN_ROOT_ID = "synthetic_hands" diff --git a/examples/teleop/python/gamepad_printer_example.py b/examples/teleop/python/isaaccapture_examples/teleop/gamepad_printer_example.py similarity index 100% rename from examples/teleop/python/gamepad_printer_example.py rename to examples/teleop/python/isaaccapture_examples/teleop/gamepad_printer_example.py diff --git a/examples/teleop/python/gripper_retargeting_example.py b/examples/teleop/python/isaaccapture_examples/teleop/gripper_retargeting_example.py similarity index 93% rename from examples/teleop/python/gripper_retargeting_example.py rename to examples/teleop/python/isaaccapture_examples/teleop/gripper_retargeting_example.py index c52a8ba83c..cfa05e47b5 100644 --- a/examples/teleop/python/gripper_retargeting_example.py +++ b/examples/teleop/python/isaaccapture_examples/teleop/gripper_retargeting_example.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 """ @@ -36,7 +36,9 @@ ) -PLUGIN_ROOT_DIR = Path(__file__).resolve().parent.parent.parent.parent / "plugins" +# Six levels up is the repo root in a checkout or editable install. It has no +# plugins/ directory, so this example runs without the synthetic-hands plugin. +PLUGIN_ROOT_DIR = Path(__file__).resolve().parents[5] / "plugins" PLUGIN_NAME = "controller_synthetic_hands" PLUGIN_ROOT_ID = "synthetic_hands" diff --git a/examples/teleop/python/gripper_retargeting_example_simple.py b/examples/teleop/python/isaaccapture_examples/teleop/gripper_retargeting_example_simple.py similarity index 89% rename from examples/teleop/python/gripper_retargeting_example_simple.py rename to examples/teleop/python/isaaccapture_examples/teleop/gripper_retargeting_example_simple.py index be1d5ca328..f5e9873467 100644 --- a/examples/teleop/python/gripper_retargeting_example_simple.py +++ b/examples/teleop/python/isaaccapture_examples/teleop/gripper_retargeting_example_simple.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 """ @@ -13,7 +13,7 @@ from pathlib import Path import isaaccapture.deviceio as deviceio -from _example_loop import add_duration_argument, run_example_loop +from ._example_loop import add_duration_argument, run_example_loop from isaaccapture.cloudxr import CloudXRLauncher from isaaccapture.retargeters import ( @@ -28,7 +28,9 @@ ) -PLUGIN_ROOT_DIR = Path(__file__).resolve().parent.parent.parent.parent / "plugins" +# Six levels up is the repo root in a checkout or editable install. It has no +# plugins/ directory, so this example runs without the synthetic-hands plugin. +PLUGIN_ROOT_DIR = Path(__file__).resolve().parents[5] / "plugins" PLUGIN_NAME = "controller_synthetic_hands" PLUGIN_ROOT_ID = "synthetic_hands" diff --git a/examples/teleop/python/isaac_lab_gripper_example.py b/examples/teleop/python/isaaccapture_examples/teleop/isaac_lab_gripper_example.py similarity index 96% rename from examples/teleop/python/isaac_lab_gripper_example.py rename to examples/teleop/python/isaaccapture_examples/teleop/isaac_lab_gripper_example.py index c6a3a3ee71..1589358129 100644 --- a/examples/teleop/python/isaac_lab_gripper_example.py +++ b/examples/teleop/python/isaaccapture_examples/teleop/isaac_lab_gripper_example.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 """ diff --git a/examples/teleop/python/joint_space_device_example.py b/examples/teleop/python/isaaccapture_examples/teleop/joint_space_device_example.py similarity index 87% rename from examples/teleop/python/joint_space_device_example.py rename to examples/teleop/python/isaaccapture_examples/teleop/joint_space_device_example.py index 70fbd4dd58..239437c92e 100644 --- a/examples/teleop/python/joint_space_device_example.py +++ b/examples/teleop/python/isaaccapture_examples/teleop/joint_space_device_example.py @@ -25,13 +25,14 @@ Examples:: - python joint_space_device_example.py --launch-plugin --mode joint --frames 8 - python joint_space_device_example.py --launch-plugin --mode ee --urdf /path/to/so101_new_calib.urdf + python -m isaaccapture_examples.teleop.joint_space_device_example --launch-plugin --mode joint --frames 8 + python -m isaaccapture_examples.teleop.joint_space_device_example --launch-plugin --mode ee --urdf /path/to/so101_new_calib.urdf """ from __future__ import annotations import argparse +import shutil import subprocess import time from pathlib import Path @@ -59,10 +60,19 @@ _COLLECTION_ID = "so101_leader" _POSE_LABELS = ["pos_x", "pos_y", "pos_z", "quat_x", "quat_y", "quat_z", "quat_w"] -_DEFAULT_PLUGIN_BIN = ( - Path(__file__).resolve().parents[3] - / "build/src/plugins/so101_leader/so101_leader_plugin" -) + + +def _default_plugin_bin() -> str: + # Six levels up is the repo root in a checkout or editable install. A wheel + # install puts this file in site-packages, where neither path exists. + root = Path(__file__).resolve().parents[5] + for cand in ( + root / "build/src/plugins/so101_leader/so101_leader_plugin", + root / "install/plugins/so101_leader/so101_leader_plugin", + ): + if cand.exists(): + return str(cand) + return "so101_leader_plugin" # rely on PATH def build_pipeline( @@ -201,8 +211,8 @@ def main() -> None: ) parser.add_argument( "--plugin-bin", - default=str(_DEFAULT_PLUGIN_BIN), - help="Path to so101_leader_plugin", + default=_default_plugin_bin(), + help="Path to so101_leader_plugin, or its name on PATH", ) parser.add_argument( "--timeout", @@ -215,13 +225,15 @@ def main() -> None: plugin_proc = None if args.launch_plugin: - if not Path(args.plugin_bin).exists(): + plugin_bin = shutil.which(args.plugin_bin) + if plugin_bin is None: raise SystemExit( - f"plugin binary not found: {args.plugin_bin} (build it first)" + f"plugin binary not found: {args.plugin_bin} " + "(build it first, or pass --plugin-bin)" ) - print(f"launching plugin: {args.plugin_bin}") + print(f"launching plugin: {plugin_bin}") # Empty device_path -> synthetic backend; collection id must match the source. - plugin_proc = subprocess.Popen([args.plugin_bin, "", _COLLECTION_ID]) + plugin_proc = subprocess.Popen([plugin_bin, "", _COLLECTION_ID]) time.sleep(1.5) # let it create its OpenXR session and start pushing try: with CloudXRLauncher.launch_context(args): diff --git a/examples/teleop/python/keyboard_printer_example.py b/examples/teleop/python/isaaccapture_examples/teleop/keyboard_printer_example.py similarity index 100% rename from examples/teleop/python/keyboard_printer_example.py rename to examples/teleop/python/isaaccapture_examples/teleop/keyboard_printer_example.py diff --git a/examples/teleop/python/keyboard_terminal_example.py b/examples/teleop/python/isaaccapture_examples/teleop/keyboard_terminal_example.py similarity index 99% rename from examples/teleop/python/keyboard_terminal_example.py rename to examples/teleop/python/isaaccapture_examples/teleop/keyboard_terminal_example.py index 326b27f20e..e34a01c37b 100644 --- a/examples/teleop/python/keyboard_terminal_example.py +++ b/examples/teleop/python/isaaccapture_examples/teleop/keyboard_terminal_example.py @@ -17,7 +17,7 @@ Like every TeleopSession it opens an OpenXR session; the CloudXR launcher arguments start a runtime, and no headset needs to connect: - python keyboard_terminal_example.py + python -m isaaccapture_examples.teleop.keyboard_terminal_example """ import argparse diff --git a/examples/teleop/python/locomotion_retargeting_example.py b/examples/teleop/python/isaaccapture_examples/teleop/locomotion_retargeting_example.py similarity index 92% rename from examples/teleop/python/locomotion_retargeting_example.py rename to examples/teleop/python/isaaccapture_examples/teleop/locomotion_retargeting_example.py index 14efd9dba2..32549c1e92 100644 --- a/examples/teleop/python/locomotion_retargeting_example.py +++ b/examples/teleop/python/isaaccapture_examples/teleop/locomotion_retargeting_example.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 """ @@ -27,7 +27,9 @@ ) -PLUGIN_ROOT_DIR = Path(__file__).resolve().parent.parent.parent.parent / "plugins" +# Six levels up is the repo root in a checkout or editable install. It has no +# plugins/ directory, so this example runs without the synthetic-hands plugin. +PLUGIN_ROOT_DIR = Path(__file__).resolve().parents[5] / "plugins" PLUGIN_NAME = "controller_synthetic_hands" PLUGIN_ROOT_ID = "synthetic_hands" diff --git a/examples/teleop/python/se3_retargeting_example.py b/examples/teleop/python/isaaccapture_examples/teleop/se3_retargeting_example.py similarity index 96% rename from examples/teleop/python/se3_retargeting_example.py rename to examples/teleop/python/isaaccapture_examples/teleop/se3_retargeting_example.py index 3a273bd086..5e78fbe482 100644 --- a/examples/teleop/python/se3_retargeting_example.py +++ b/examples/teleop/python/isaaccapture_examples/teleop/se3_retargeting_example.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 """ @@ -12,7 +12,7 @@ import sys import numpy as np -from _example_loop import add_duration_argument, run_example_loop +from ._example_loop import add_duration_argument, run_example_loop from isaaccapture.cloudxr import CloudXRLauncher from isaaccapture.retargeting_engine.deviceio_source_nodes import ( diff --git a/examples/teleop/python/spacemouse_printer_example.py b/examples/teleop/python/isaaccapture_examples/teleop/spacemouse_printer_example.py similarity index 100% rename from examples/teleop/python/spacemouse_printer_example.py rename to examples/teleop/python/isaaccapture_examples/teleop/spacemouse_printer_example.py diff --git a/examples/teleop/python/pyproject.toml b/examples/teleop/python/pyproject.toml deleted file mode 100644 index 832ec1e1f3..0000000000 --- a/examples/teleop/python/pyproject.toml +++ /dev/null @@ -1,12 +0,0 @@ -# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -# SPDX-License-Identifier: Apache-2.0 - -[project] -name = "teleop-examples" -version = "0.1.0" -description = "Isaac Teleop Retargeting Examples" -requires-python = ">=3.11,<3.14" -dependencies = [ - "isaaccapture[retargeters]", - "glfw>=2.7", # keyboard_printer_example: the window and its platform constants -] diff --git a/src/plugins/so101_leader/README.md b/src/plugins/so101_leader/README.md index 20c9e4c09c..a24faa3de3 100644 --- a/src/plugins/so101_leader/README.md +++ b/src/plugins/so101_leader/README.md @@ -121,7 +121,8 @@ absorb calibration; for **EE (URDF FK)** mode the joint angles must already matc The consumer side creates a `JointStateTracker("so101_leader")` (via `JointStateSource(name=..., collection_id="so101_leader", joint_names=[...])`) on the same -`collection_id`. See `examples/teleop/python/joint_space_device_example.py` for the retargeting -pipeline (joint-mirror and task-space EE modes). +`collection_id`. See +`examples/teleop/python/isaaccapture_examples/teleop/joint_space_device_example.py` for the +retargeting pipeline (joint-mirror and task-space EE modes). DOF order / names: `shoulder_pan, shoulder_lift, elbow_flex, wrist_flex, wrist_roll, gripper`. diff --git a/src/python/isaaccapture/retargeters/joint_space/joint_state_retargeter.py b/src/python/isaaccapture/retargeters/joint_space/joint_state_retargeter.py index 0654f67248..45ebca6197 100644 --- a/src/python/isaaccapture/retargeters/joint_space/joint_state_retargeter.py +++ b/src/python/isaaccapture/retargeters/joint_space/joint_state_retargeter.py @@ -18,8 +18,9 @@ The output element names are chosen so a downstream :class:`~isaaccapture.retargeters.TensorReorderer` can flatten them into the exact action layout -an Isaac Lab environment expects. See ``examples/teleop/python/joint_space_device_example.py`` -for an end-to-end reference. +an Isaac Lab environment expects. See +``examples/teleop/python/isaaccapture_examples/teleop/joint_space_device_example.py`` for an +end-to-end reference. """ from __future__ import annotations diff --git a/tests/python/examples/teleop/test_retargeting_examples.py b/tests/python/examples/teleop/test_retargeting_examples.py index 96e38204b7..08a85a0ca4 100644 --- a/tests/python/examples/teleop/test_retargeting_examples.py +++ b/tests/python/examples/teleop/test_retargeting_examples.py @@ -122,9 +122,13 @@ def module(name, **attributes): HandsSource=MagicMock(), ControllersSource=MagicMock(), ) - directory = repo_root() / "examples" / "teleop" / "python" - monkeypatch.syspath_prepend(str(directory)) - spec = importlib.util.spec_from_file_location(name, directory / f"{name}.py") + # python/ is the namespace root, so the relative `._example_loop` import resolves. + namespace_root = repo_root() / "examples" / "teleop" / "python" + monkeypatch.syspath_prepend(str(namespace_root)) + package = namespace_root / "isaaccapture_examples" / "teleop" + spec = importlib.util.spec_from_file_location( + f"isaaccapture_examples.teleop.{name}", package / f"{name}.py" + ) loaded = importlib.util.module_from_spec(spec) spec.loader.exec_module(loaded) monkeypatch.setattr( @@ -142,8 +146,9 @@ def sleep(_): ) if hasattr(loaded, "time"): monkeypatch.setattr(loaded, "time", clock) - if "_example_loop" in sys.modules: - monkeypatch.setattr(sys.modules["_example_loop"], "time", clock) + example_loop = sys.modules.get("isaaccapture_examples.teleop._example_loop") + if example_loop is not None: + monkeypatch.setattr(example_loop, "time", clock) def run(*args): monkeypatch.setattr(sys, "argv", [f"{name}.py", *args]) diff --git a/tests/python/examples/teleop_keyboard/CMakeLists.txt b/tests/python/examples/teleop_keyboard/CMakeLists.txt index be1a157ea4..4fc1c8e6c3 100644 --- a/tests/python/examples/teleop_keyboard/CMakeLists.txt +++ b/tests/python/examples/teleop_keyboard/CMakeLists.txt @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # -# Unit tests for the keyboard examples under examples/teleop/python. No display is +# Unit tests for the keyboard examples under examples/teleop. No display is # needed: the GLFW test never opens a window, and the terminal test runs against a # pseudo-terminal. Neither opens a TeleopSession. diff --git a/tests/python/examples/teleop_keyboard/conftest.py b/tests/python/examples/teleop_keyboard/conftest.py index 28b5d6aad6..357316a7e8 100644 --- a/tests/python/examples/teleop_keyboard/conftest.py +++ b/tests/python/examples/teleop_keyboard/conftest.py @@ -1,7 +1,11 @@ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -# The examples under test live in examples/teleop/python, which is not a package. +# Resolve `isaaccapture_examples.teleop` against the in-tree source. +# +# python/, not python/isaaccapture_examples/: that is a PEP 420 namespace. Do not +# add an __init__.py to make an import work -- it breaks the installed wheel's +# ability to share the namespace. import sys from pathlib import Path diff --git a/tests/python/examples/teleop_keyboard/test_keyboard_printer_example.py b/tests/python/examples/teleop_keyboard/test_keyboard_printer_example.py index ff4e7cfc79..b51a9c025c 100644 --- a/tests/python/examples/teleop_keyboard/test_keyboard_printer_example.py +++ b/tests/python/examples/teleop_keyboard/test_keyboard_printer_example.py @@ -6,7 +6,9 @@ import glfw import pytest -from keyboard_printer_example import evdev_code_from_scancode +from isaaccapture_examples.teleop.keyboard_printer_example import ( + evdev_code_from_scancode, +) KEY_W, KEY_UP, KEY_F1 = 17, 103, 59 diff --git a/tests/python/examples/teleop_keyboard/test_keyboard_terminal_example.py b/tests/python/examples/teleop_keyboard/test_keyboard_terminal_example.py index 6b4a28dae0..3974a5a573 100644 --- a/tests/python/examples/teleop_keyboard/test_keyboard_terminal_example.py +++ b/tests/python/examples/teleop_keyboard/test_keyboard_terminal_example.py @@ -14,8 +14,8 @@ import pytest -import keyboard_terminal_example -from keyboard_terminal_example import TerminalKeySource +from isaaccapture_examples.teleop import keyboard_terminal_example +from isaaccapture_examples.teleop.keyboard_terminal_example import TerminalKeySource KITTY_REPLY = ( b"\x1b[?1u\x1b[?62c" # kitty protocol flags, then the primary device attributes