diff --git a/.gitignore b/.gitignore index f939df2136..9eaed84a97 100644 --- a/.gitignore +++ b/.gitignore @@ -67,6 +67,10 @@ MUJOCO_LOG.TXT # and its README has you run from the repo root. /recordings/ +# examples/lerobot writes here by default (Path.cwd() / "local_datasets"), +# and its README has you run from the repo root. +/local_datasets/ + # Runtime device files injected by MCP server infrastructure .mcp.json diff --git a/docs/source/device/haptic_feedback.rst b/docs/source/device/haptic_feedback.rst index 9e7800cb75..271834877e 100644 --- a/docs/source/device/haptic_feedback.rst +++ b/docs/source/device/haptic_feedback.rst @@ -50,7 +50,7 @@ graph outputs and writes them *out* to a device after the graph runs. Example ------- -``examples/haptic_feedback/python/controller_haptic_example.py`` is the minimal +``examples/haptic_feedback`` provides the minimal end-to-end wiring: pull a controller's trigger and that **same** controller rumbles. ``TriggerToTactile`` turns the trigger value into a ``TactileVector``, ``TactileVectorToControllerPulse`` turns that into a ``ControllerHapticPulse``, @@ -67,12 +67,12 @@ Running The example connects through the CloudXR / OpenXR runtime, which it starts itself if nothing is serving (see :ref:`dedicated-cloudxr-runtime` to manage -that service yourself). Run it from the ``examples/haptic_feedback/python`` -directory: +that service yourself). Install the example, then run it: .. code-block:: bash - uv run controller_haptic_example.py + uv pip install -e ./examples/haptic_feedback + python -m isaaccapture_examples.haptic_feedback.controller_haptic_example No arguments — pull either trigger to rumble that controller. Press Ctrl+C to exit. Runtimes that do not expose ``xrApplyHapticFeedback`` silently no-op rather @@ -94,6 +94,6 @@ foundation. See also -------- -- Example + tests: ``examples/haptic_feedback/python/`` and +- Example + tests: ``examples/haptic_feedback`` and ``tests/python/core/retargeting_engine/test_haptic_devices.py`` / ``test_haptic_sink.py``. diff --git a/examples/cloudxr_mujoco_teleop/README.md b/examples/cloudxr_mujoco_teleop/README.md index f384b2362b..aac587a00b 100644 --- a/examples/cloudxr_mujoco_teleop/README.md +++ b/examples/cloudxr_mujoco_teleop/README.md @@ -13,8 +13,8 @@ works with any CloudXR / OpenXR-compatible HMD. | Example | What it shows | |---------|---------------| -| `visualize_poses_mujoco_example.py` | HMD + left/right controller as textured mocap bodies. | -| `visualize_hands_mujoco_example.py` | Both hands as 26-joint skeletons (spheres + capsule bones), with the HMD as optional context. | +| `visualize_poses_mujoco_example` | HMD + left/right controller as textured mocap bodies. | +| `visualize_hands_mujoco_example` | Both hands as 26-joint skeletons (spheres + capsule bones), with the HMD as optional context. | ## Prerequisites @@ -24,9 +24,11 @@ Assumes IsaacTeleop is already built and installed in this tree. ```bash curl -LsSf https://astral.sh/uv/install.sh | sh ``` - On first launch, `uv run` fetches this example's Python dependencies - (`mujoco`, `numpy`, `scipy` — see `pyproject.toml`) into an isolated - environment. + Install the example once; this pulls its Python dependencies + (`mujoco`, `numpy`, `scipy` — see `pyproject.toml`): + ```bash + uv pip install -e ./examples/cloudxr_mujoco_teleop + ``` 2. **CloudXR environment loaded**, so OpenXR resolves to the CloudXR runtime. Source the setup script that ships with your CloudXR install: @@ -41,12 +43,12 @@ limit, no recording. ## Examples -### `visualize_poses_mujoco_example.py` +### `visualize_poses_mujoco_example` HMD + left + right controller, each rendered as a textured mesh mocap body. ```bash -uv run visualize_poses_mujoco_example.py +python -m isaaccapture_examples.cloudxr_mujoco_teleop.visualize_poses_mujoco_example ``` | Flag | Default | Description | @@ -55,7 +57,7 @@ uv run visualize_poses_mujoco_example.py | `--pose {grip,aim}` | `grip` | Which controller pose drives the mesh — natural "hold" pose or the aim ray. | | `--debug` | off | Print `mocap_pos` for all three devices once per second. | -### `visualize_hands_mujoco_example.py` +### `visualize_hands_mujoco_example` Both hands as 26-joint skeletons drawn directly into `viewer.user_scn` each frame (left = cyan, right = warm orange) — avoids paying for 52 mocap bodies @@ -63,7 +65,7 @@ in the model. The HMD is shown as context using the same mesh + texture as the poses example. ```bash -uv run visualize_hands_mujoco_example.py +python -m isaaccapture_examples.cloudxr_mujoco_teleop.visualize_hands_mujoco_example ``` | Flag | Default | Description | @@ -92,16 +94,17 @@ HMD will be visible. ``` cloudxr_mujoco_teleop/ ├── README.md -├── pyproject.toml # uv dependency declaration -├── visualize_poses_mujoco_example.py -├── visualize_hands_mujoco_example.py -└── vive_assets/ - ├── generic_hmd.obj # HMD mesh (~500 KB) - ├── generic_hmd_color.png # HMD texture - ├── vive_focus3_controller_left.obj - ├── vive_focus3_controller_left_color.png - ├── vive_focus3_controller_right.obj - └── vive_focus3_controller_right_color.png +├── pyproject.toml # the distribution +└── python/isaaccapture_examples/cloudxr_mujoco_teleop/ + ├── visualize_poses_mujoco_example.py + ├── visualize_hands_mujoco_example.py + └── vive_assets/ + ├── generic_hmd.obj # HMD mesh (~500 KB) + ├── generic_hmd_color.png # HMD texture + ├── vive_focus3_controller_left.obj + ├── vive_focus3_controller_left_color.png + ├── vive_focus3_controller_right.obj + └── vive_focus3_controller_right_color.png ``` The bundled meshes are a generic HMD and the Vive Focus 3 controllers. diff --git a/examples/cloudxr_mujoco_teleop/pyproject.toml b/examples/cloudxr_mujoco_teleop/pyproject.toml index a3649ef846..12a0f56155 100644 --- a/examples/cloudxr_mujoco_teleop/pyproject.toml +++ b/examples/cloudxr_mujoco_teleop/pyproject.toml @@ -1,8 +1,14 @@ # SPDX-FileCopyrightText: Copyright (c) 2026 HTC CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + [project] -name = "cloudxr-mujoco-teleop-example" +# The dist name mirrors the import path, so an installed example claims no bare +# top-level name in site-packages. +name = "isaaccapture-examples-cloudxr-mujoco-teleop" version = "0.1.0" description = "Visualize OpenXR / CloudXR headset + controller poses in MuJoCo." requires-python = ">=3.11,<3.14" @@ -12,3 +18,14 @@ dependencies = [ "numpy>=1.22.2", "scipy>=1.10", ] + +# `isaaccapture_examples` is a PEP 420 namespace shared by every example dist and +# must stay without an __init__.py. `only-include` + `sources`, not `packages`: +# the latter keeps only the last path component and would root the wheel at a +# bare top-level `cloudxr_mujoco_teleop/`. +# +# vive_assets/ lives inside the package because both scripts resolve it from +# __file__, so the meshes must travel with the modules. +[tool.hatch.build.targets.wheel] +only-include = ["python/isaaccapture_examples/cloudxr_mujoco_teleop"] +sources = ["python"] diff --git a/examples/cloudxr_mujoco_teleop/python/isaaccapture_examples/cloudxr_mujoco_teleop/__init__.py b/examples/cloudxr_mujoco_teleop/python/isaaccapture_examples/cloudxr_mujoco_teleop/__init__.py new file mode 100644 index 0000000000..f3e5d4b141 --- /dev/null +++ b/examples/cloudxr_mujoco_teleop/python/isaaccapture_examples/cloudxr_mujoco_teleop/__init__.py @@ -0,0 +1,2 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 HTC CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 diff --git a/examples/cloudxr_mujoco_teleop/visualize_hands_mujoco_example.py b/examples/cloudxr_mujoco_teleop/python/isaaccapture_examples/cloudxr_mujoco_teleop/visualize_hands_mujoco_example.py similarity index 100% rename from examples/cloudxr_mujoco_teleop/visualize_hands_mujoco_example.py rename to examples/cloudxr_mujoco_teleop/python/isaaccapture_examples/cloudxr_mujoco_teleop/visualize_hands_mujoco_example.py diff --git a/examples/cloudxr_mujoco_teleop/visualize_poses_mujoco_example.py b/examples/cloudxr_mujoco_teleop/python/isaaccapture_examples/cloudxr_mujoco_teleop/visualize_poses_mujoco_example.py similarity index 100% rename from examples/cloudxr_mujoco_teleop/visualize_poses_mujoco_example.py rename to examples/cloudxr_mujoco_teleop/python/isaaccapture_examples/cloudxr_mujoco_teleop/visualize_poses_mujoco_example.py diff --git a/examples/cloudxr_mujoco_teleop/vive_assets/generic_hmd.obj b/examples/cloudxr_mujoco_teleop/python/isaaccapture_examples/cloudxr_mujoco_teleop/vive_assets/generic_hmd.obj similarity index 100% rename from examples/cloudxr_mujoco_teleop/vive_assets/generic_hmd.obj rename to examples/cloudxr_mujoco_teleop/python/isaaccapture_examples/cloudxr_mujoco_teleop/vive_assets/generic_hmd.obj diff --git a/examples/cloudxr_mujoco_teleop/vive_assets/generic_hmd_color.png b/examples/cloudxr_mujoco_teleop/python/isaaccapture_examples/cloudxr_mujoco_teleop/vive_assets/generic_hmd_color.png similarity index 100% rename from examples/cloudxr_mujoco_teleop/vive_assets/generic_hmd_color.png rename to examples/cloudxr_mujoco_teleop/python/isaaccapture_examples/cloudxr_mujoco_teleop/vive_assets/generic_hmd_color.png diff --git a/examples/cloudxr_mujoco_teleop/vive_assets/vive_focus3_controller_left.obj b/examples/cloudxr_mujoco_teleop/python/isaaccapture_examples/cloudxr_mujoco_teleop/vive_assets/vive_focus3_controller_left.obj similarity index 100% rename from examples/cloudxr_mujoco_teleop/vive_assets/vive_focus3_controller_left.obj rename to examples/cloudxr_mujoco_teleop/python/isaaccapture_examples/cloudxr_mujoco_teleop/vive_assets/vive_focus3_controller_left.obj diff --git a/examples/cloudxr_mujoco_teleop/vive_assets/vive_focus3_controller_left_color.png b/examples/cloudxr_mujoco_teleop/python/isaaccapture_examples/cloudxr_mujoco_teleop/vive_assets/vive_focus3_controller_left_color.png similarity index 100% rename from examples/cloudxr_mujoco_teleop/vive_assets/vive_focus3_controller_left_color.png rename to examples/cloudxr_mujoco_teleop/python/isaaccapture_examples/cloudxr_mujoco_teleop/vive_assets/vive_focus3_controller_left_color.png diff --git a/examples/cloudxr_mujoco_teleop/vive_assets/vive_focus3_controller_right.obj b/examples/cloudxr_mujoco_teleop/python/isaaccapture_examples/cloudxr_mujoco_teleop/vive_assets/vive_focus3_controller_right.obj similarity index 100% rename from examples/cloudxr_mujoco_teleop/vive_assets/vive_focus3_controller_right.obj rename to examples/cloudxr_mujoco_teleop/python/isaaccapture_examples/cloudxr_mujoco_teleop/vive_assets/vive_focus3_controller_right.obj diff --git a/examples/cloudxr_mujoco_teleop/vive_assets/vive_focus3_controller_right_color.png b/examples/cloudxr_mujoco_teleop/python/isaaccapture_examples/cloudxr_mujoco_teleop/vive_assets/vive_focus3_controller_right_color.png similarity index 100% rename from examples/cloudxr_mujoco_teleop/vive_assets/vive_focus3_controller_right_color.png rename to examples/cloudxr_mujoco_teleop/python/isaaccapture_examples/cloudxr_mujoco_teleop/vive_assets/vive_focus3_controller_right_color.png diff --git a/examples/haptic_feedback/CMakeLists.txt b/examples/haptic_feedback/CMakeLists.txt index 0bc23be68f..9e640c2880 100644 --- a/examples/haptic_feedback/CMakeLists.txt +++ b/examples/haptic_feedback/CMakeLists.txt @@ -4,4 +4,4 @@ cmake_minimum_required(VERSION 3.20) include(${CMAKE_SOURCE_DIR}/cmake/InstallPythonExample.cmake) -install_python_example(DESTINATION examples/haptic_feedback/python) +install_python_example(DESTINATION examples/haptic_feedback) diff --git a/examples/haptic_feedback/python/README.md b/examples/haptic_feedback/README.md similarity index 55% rename from examples/haptic_feedback/python/README.md rename to examples/haptic_feedback/README.md index 2897c71ad7..317ee13ee8 100644 --- a/examples/haptic_feedback/python/README.md +++ b/examples/haptic_feedback/README.md @@ -11,12 +11,13 @@ flushes to an `IHapticDevice` adapter each frame. | Example | What it demonstrates | | --- | --- | -| `controller_haptic_example.py` | Pull a controller trigger to rumble that same controller — the minimal in-process `HapticSink` → `ControllerHapticDevice` wiring. | -| `hand_pinch_haptic_example.py` | Pinch a fingertip toward the thumb to vibrate a haptic glove — the cross-process path (`HapticSink` → `PushTensorHapticDevice`, pushing a `HapticCommand` to a glove plugin such as Manus). | +| `controller_haptic_example` | Pull a controller trigger to rumble that same controller — the minimal in-process `HapticSink` → `ControllerHapticDevice` wiring. | +| `hand_pinch_haptic_example` | Pinch a fingertip toward the thumb to vibrate a haptic glove — the cross-process path (`HapticSink` → `PushTensorHapticDevice`, pushing a `HapticCommand` to a glove plugin such as Manus). | ```bash -uv run controller_haptic_example.py # motion-controller rumble -uv run hand_pinch_haptic_example.py # haptic glove (needs a glove plugin running) +uv pip install -e ./examples/haptic_feedback +python -m isaaccapture_examples.haptic_feedback.controller_haptic_example # motion-controller rumble +python -m isaaccapture_examples.haptic_feedback.hand_pinch_haptic_example # haptic glove (needs a glove plugin running) ``` Both connect through the CloudXR / OpenXR runtime, so start the runtime first. diff --git a/examples/haptic_feedback/pyproject.toml b/examples/haptic_feedback/pyproject.toml new file mode 100644 index 0000000000..90ab2f0d6f --- /dev/null +++ b/examples/haptic_feedback/pyproject.toml @@ -0,0 +1,26 @@ +# 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-haptic-feedback" +version = "0.0.0" # Internal examples - not versioned +description = "Isaac Teleop haptic feedback CLI examples (OpenXR controller)." +requires-python = ">=3.11,<3.14" +dependencies = [ + "isaaccapture", + "numpy>=1.23.0", +] + +# `isaaccapture_examples` is a PEP 420 namespace shared by every example dist and +# must stay without an __init__.py. `only-include` + `sources`, not `packages`: +# the latter keeps only the last path component and would root the wheel at a +# bare top-level `haptic_feedback/`. +[tool.hatch.build.targets.wheel] +only-include = ["python/isaaccapture_examples/haptic_feedback"] +sources = ["python"] diff --git a/examples/haptic_feedback/python/isaaccapture_examples/haptic_feedback/__init__.py b/examples/haptic_feedback/python/isaaccapture_examples/haptic_feedback/__init__.py new file mode 100644 index 0000000000..52a7a9daf0 --- /dev/null +++ b/examples/haptic_feedback/python/isaaccapture_examples/haptic_feedback/__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/haptic_feedback/python/controller_haptic_example.py b/examples/haptic_feedback/python/isaaccapture_examples/haptic_feedback/controller_haptic_example.py similarity index 100% rename from examples/haptic_feedback/python/controller_haptic_example.py rename to examples/haptic_feedback/python/isaaccapture_examples/haptic_feedback/controller_haptic_example.py diff --git a/examples/haptic_feedback/python/hand_pinch_haptic_example.py b/examples/haptic_feedback/python/isaaccapture_examples/haptic_feedback/hand_pinch_haptic_example.py similarity index 100% rename from examples/haptic_feedback/python/hand_pinch_haptic_example.py rename to examples/haptic_feedback/python/isaaccapture_examples/haptic_feedback/hand_pinch_haptic_example.py diff --git a/examples/haptic_feedback/python/pyproject.toml b/examples/haptic_feedback/python/pyproject.toml deleted file mode 100644 index 6472ad2986..0000000000 --- a/examples/haptic_feedback/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 = "isaaccapture-haptic-feedback-examples" -version = "0.0.0" # Internal examples - not versioned -description = "Isaac Teleop haptic feedback CLI examples (OpenXR controller)." -requires-python = ">=3.11,<3.14" -dependencies = [ - "isaaccapture", - "numpy>=1.23.0", -] diff --git a/examples/lerobot/README.md b/examples/lerobot/README.md index 3829d0b579..81c2bc832b 100644 --- a/examples/lerobot/README.md +++ b/examples/lerobot/README.md @@ -3,32 +3,30 @@ SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. SPDX-License-Identifier: Apache-2.0 --> -# LeRobot Example Datasets +# LeRobot example datasets -This directory contains minimal usage examples for recording, visualizing and -analyzing LeRobot datasets. +Minimal examples for recording, visualizing and analyzing +[LeRobot](https://github.com/huggingface/lerobot) datasets from Isaac Teleop. -## Prerequisite - -``` -pip install lerobot +```bash +uv pip install -e ./examples/lerobot # add [viz] for the rerun viewer +python -m isaaccapture_examples.lerobot.record ``` -## Examples - -- **record.py** - Record a dataset in the LeRobot format from live human data. Currently it only - captures head and hands position for demonstrations purpose. +| Module | What it does | +| --- | --- | +| `record` | Records a LeRobot-format dataset from live human data — head and hand positions only, for demonstration | +| `visualize` | Plots a dataset with [rerun](https://rerun.io) (needs the `viz` extra) | +| `analyze` | Parses and summarizes a recorded dataset | - Note: the record.py script always create a new dataset. You must remove - existing one before running it again: +Datasets are written to `./local_datasets/` relative to where you run the +command, and the other two modules read from the same place. `record` always +creates a new dataset, so remove an old one before re-running: - ```bash - rm -rf local_datasets - ``` - -- **visualize.py** - A basic rerun visualizer to plot out the dataset. +```bash +rm -rf local_datasets +``` -- **analyze.py** - A quick sample to parse and analyze the LeRobot dataset. +> The SO-101 teleoperation example referenced from the Isaac Teleop docs lives +> in the [LeRobot repository](https://github.com/huggingface/lerobot) under +> `examples/isaac_teleop_to_so101/`, not here. diff --git a/examples/lerobot/pyproject.toml b/examples/lerobot/pyproject.toml new file mode 100644 index 0000000000..ba686d3dd8 --- /dev/null +++ b/examples/lerobot/pyproject.toml @@ -0,0 +1,31 @@ +# 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-lerobot" +version = "0.0.0" # Internal example - not versioned +description = "Record, visualize and analyze LeRobot datasets from Isaac Teleop" +requires-python = ">=3.11,<3.14" +dependencies = [ + "isaaccapture[cloudxr]", + "lerobot", + "numpy>=1.23.0", +] + +[project.optional-dependencies] +# visualize.py only; recording and analysis need no viewer. +viz = ["rerun-sdk"] + +# `isaaccapture_examples` is a PEP 420 namespace shared by every example dist and +# must stay without an __init__.py. `only-include` + `sources`, not `packages`: +# the latter keeps only the last path component and would root the wheel at a +# bare top-level `lerobot/` -- which would sit next to the real `lerobot`. +[tool.hatch.build.targets.wheel] +only-include = ["python/isaaccapture_examples/lerobot"] +sources = ["python"] diff --git a/examples/lerobot/python/isaaccapture_examples/lerobot/__init__.py b/examples/lerobot/python/isaaccapture_examples/lerobot/__init__.py new file mode 100644 index 0000000000..52a7a9daf0 --- /dev/null +++ b/examples/lerobot/python/isaaccapture_examples/lerobot/__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/lerobot/analyze.py b/examples/lerobot/python/isaaccapture_examples/lerobot/analyze.py similarity index 100% rename from examples/lerobot/analyze.py rename to examples/lerobot/python/isaaccapture_examples/lerobot/analyze.py diff --git a/examples/lerobot/record.py b/examples/lerobot/python/isaaccapture_examples/lerobot/record.py similarity index 97% rename from examples/lerobot/record.py rename to examples/lerobot/python/isaaccapture_examples/lerobot/record.py index b92ea372de..3d236c90dd 100644 --- a/examples/lerobot/record.py +++ b/examples/lerobot/python/isaaccapture_examples/lerobot/record.py @@ -69,7 +69,9 @@ def main(): # STEP 2: Create LeRobot dataset # Use a timestamped directory so repeated runs create unique datasets timestamp = time.strftime("%Y%m%d_%H%M%S") - dataset_path = Path(__file__).parent / f"local_datasets/teleop_tracking_{timestamp}" + # CWD-relative, matching what visualize.py and analyze.py read. Anchoring + # this to __file__ would write inside the installed package. + dataset_path = Path.cwd() / f"local_datasets/teleop_tracking_{timestamp}" dataset = LeRobotDataset.create( repo_id="teleop/tracking_demo", fps=60, # ~60 FPS tracking diff --git a/examples/lerobot/visualize.py b/examples/lerobot/python/isaaccapture_examples/lerobot/visualize.py similarity index 100% rename from examples/lerobot/visualize.py rename to examples/lerobot/python/isaaccapture_examples/lerobot/visualize.py