From 6874feb06e9c0e562929cd878ca03dbf4efdab71 Mon Sep 17 00:00:00 2001 From: Jiwen Cai Date: Fri, 28 Aug 2026 04:45:29 +0000 Subject: [PATCH 1/6] examples(noitom): package under isaaccapture_examples Package the Noitom examples so sibling imports work outside the source directory. Part of NVIDIA/IsaacCapture#985. Signed-off-by: Jiwen Cai Signed-off-by: Ivan Yang --- examples/noitom/README.md | 24 ++++++++++----- examples/noitom/pyproject.toml | 29 +++++++++++++++++++ .../isaaccapture_examples/noitom/__init__.py | 2 ++ .../noitom}/noitom_reference_draw.py | 2 +- .../noitom}/noitom_retargeting.py | 2 +- .../noitom}/noitom_tasks.py | 8 +++-- .../noitom}/record_noitom_full_body.py | 8 +++-- src/plugins/noitom_mocap/README.md | 3 +- 8 files changed, 62 insertions(+), 16 deletions(-) create mode 100644 examples/noitom/pyproject.toml create mode 100644 examples/noitom/python/isaaccapture_examples/noitom/__init__.py rename examples/noitom/{ => python/isaaccapture_examples/noitom}/noitom_reference_draw.py (99%) rename examples/noitom/{ => python/isaaccapture_examples/noitom}/noitom_retargeting.py (99%) rename examples/noitom/{ => python/isaaccapture_examples/noitom}/noitom_tasks.py (99%) rename examples/noitom/{ => python/isaaccapture_examples/noitom}/record_noitom_full_body.py (93%) diff --git a/examples/noitom/README.md b/examples/noitom/README.md index b9d6027f93..81c0457150 100644 --- a/examples/noitom/README.md +++ b/examples/noitom/README.md @@ -45,10 +45,11 @@ MCAP script because the recording uses the standard `full_body` channel. ![Noitom full-body recording](assets/record.gif) -Record the Noitom full-body stream: +Install the example once, then record the Noitom full-body stream: ```bash -uv run python examples/noitom/record_noitom_full_body.py \ +uv pip install -e ./examples/noitom +python -m isaaccapture_examples.noitom.record_noitom_full_body \ 10 examples/noitom/recordings/noitom_full_body.mcap ``` @@ -70,14 +71,22 @@ python -m isaaccapture_examples.mcap_record_replay.replay_full_body \ Run Isaac Lab with the external task registration callback. The task launches `noitom_mocap_plugin` through IsaacTeleop's plugin manager by default. +The callback runs inside Isaac Lab's own interpreter, so install the example +into *that* environment rather than a venv of your own — this replaces the +`PYTHONPATH=` prefix these commands used to carry: + +```bash +cd ~/dependence/IsaacLab3-0 +./isaaclab.sh -p -m pip install -e ~/IsaacTeleop/examples/noitom +``` + ```bash cd ~/dependence/IsaacLab3-0 -PYTHONPATH=~/IsaacTeleop/examples/noitom:$PYTHONPATH \ - ./isaaclab.sh -p scripts/environments/teleoperation/teleop_se3_agent.py \ +./isaaclab.sh -p scripts/environments/teleoperation/teleop_se3_agent.py \ --task Isaac-PickPlace-Locomanipulation-G1-Noitom-Abs-v0 \ --visualizer kit \ --xr \ - --external_callback noitom_tasks.register_tasks + --external_callback isaaccapture_examples.noitom.noitom_tasks.register_tasks ``` For advanced manual plugin control, start the plugin yourself and disable @@ -87,12 +96,11 @@ auto-launch in the Isaac Lab terminal: ./install/plugins/noitom_mocap/noitom_mocap_plugin NOITOM_MOCAP_AUTO_LAUNCH=0 \ -PYTHONPATH=~/IsaacTeleop/examples/noitom:$PYTHONPATH \ - ./isaaclab.sh -p scripts/environments/teleoperation/teleop_se3_agent.py \ +./isaaclab.sh -p scripts/environments/teleoperation/teleop_se3_agent.py \ --task Isaac-PickPlace-Locomanipulation-G1-Noitom-Abs-v0 \ --visualizer kit \ --xr \ - --external_callback noitom_tasks.register_tasks + --external_callback isaaccapture_examples.noitom.noitom_tasks.register_tasks ``` If you run a dedicated CloudXR runtime yourself, source its environment before diff --git a/examples/noitom/pyproject.toml b/examples/noitom/pyproject.toml new file mode 100644 index 0000000000..881601cffc --- /dev/null +++ b/examples/noitom/pyproject.toml @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: Copyright (c) 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-noitom" +version = "0.0.0" # Internal example - not versioned +description = "Noitom mocap suit examples: retargeting, Isaac Lab task, MCAP recording" +requires-python = ">=3.11,<3.14" +# isaaclab and gymnasium are deliberately absent: noitom_tasks.py runs inside an +# Isaac Lab environment that already provides them, and neither installs from +# PyPI. Install this example into that interpreter, not a fresh venv. +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 `noitom/`. +[tool.hatch.build.targets.wheel] +only-include = ["python/isaaccapture_examples/noitom"] +sources = ["python"] diff --git a/examples/noitom/python/isaaccapture_examples/noitom/__init__.py b/examples/noitom/python/isaaccapture_examples/noitom/__init__.py new file mode 100644 index 0000000000..52a7a9daf0 --- /dev/null +++ b/examples/noitom/python/isaaccapture_examples/noitom/__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/noitom/noitom_reference_draw.py b/examples/noitom/python/isaaccapture_examples/noitom/noitom_reference_draw.py similarity index 99% rename from examples/noitom/noitom_reference_draw.py rename to examples/noitom/python/isaaccapture_examples/noitom/noitom_reference_draw.py index 8e389f76dc..5bf0f178fe 100644 --- a/examples/noitom/noitom_reference_draw.py +++ b/examples/noitom/python/isaaccapture_examples/noitom/noitom_reference_draw.py @@ -13,7 +13,7 @@ from isaaccapture.schema import BodyJoint -from noitom_retargeting import noitom_position_to_isaac +from .noitom_retargeting import noitom_position_to_isaac # G1_29DOF_CFG spawn rot (0,0,0.7071,0.7071): pelvis +X aligns with world +Y (table forward). G1_ROBOT_FORWARD_XY = np.array([0.0, 1.0], dtype=np.float64) diff --git a/examples/noitom/noitom_retargeting.py b/examples/noitom/python/isaaccapture_examples/noitom/noitom_retargeting.py similarity index 99% rename from examples/noitom/noitom_retargeting.py rename to examples/noitom/python/isaaccapture_examples/noitom/noitom_retargeting.py index e7295b1dc4..768a17e66f 100644 --- a/examples/noitom/noitom_retargeting.py +++ b/examples/noitom/python/isaaccapture_examples/noitom/noitom_retargeting.py @@ -1456,7 +1456,7 @@ def _aligned_skeleton_positions( settings: NoitomRetargetingSettings, calib_view: NoitomCalibrationView, ) -> dict[int, np.ndarray]: - from noitom_reference_draw import ( + from .noitom_reference_draw import ( ReferenceSkeletonLengths, aligned_reference_skeleton_from_frame, ) diff --git a/examples/noitom/noitom_tasks.py b/examples/noitom/python/isaaccapture_examples/noitom/noitom_tasks.py similarity index 99% rename from examples/noitom/noitom_tasks.py rename to examples/noitom/python/isaaccapture_examples/noitom/noitom_tasks.py index 776d9579fb..0eacf837f6 100644 --- a/examples/noitom/noitom_tasks.py +++ b/examples/noitom/python/isaaccapture_examples/noitom/noitom_tasks.py @@ -37,13 +37,13 @@ from isaaccapture.retargeting_engine.tensor_types import DLDataType, NDArrayType from isaaccapture.teleop_session_manager import PluginConfig -from noitom_retargeting import ( +from .noitom_retargeting import ( ArmIkTargets, NoitomG1Retargeter, NoitomRetargetingSettings, noitom_position_to_isaac, ) -from noitom_reference_draw import ( +from .noitom_reference_draw import ( ReferenceSkeletonLengths, aligned_reference_skeleton_from_frame, ) @@ -168,7 +168,9 @@ def _noitom_settings_from_env() -> NoitomG1Settings: def _plugin_search_paths() -> list[Path]: - base = Path(__file__).resolve().parents[2] + # Six levels up is the repo root -- or the install prefix, which has the + # same shape under examples//python/isaaccapture_examples//. + base = Path(__file__).resolve().parents[5] candidates = [ base / "plugins", base / "install" / "plugins", diff --git a/examples/noitom/record_noitom_full_body.py b/examples/noitom/python/isaaccapture_examples/noitom/record_noitom_full_body.py similarity index 93% rename from examples/noitom/record_noitom_full_body.py rename to examples/noitom/python/isaaccapture_examples/noitom/record_noitom_full_body.py index 74f77e9f1e..d1cf959149 100644 --- a/examples/noitom/record_noitom_full_body.py +++ b/examples/noitom/python/isaaccapture_examples/noitom/record_noitom_full_body.py @@ -37,7 +37,9 @@ def _plugin_search_paths() -> list[Path]: - base = Path(__file__).resolve().parents[2] + # Six levels up is the repo root -- or the install prefix, which has the + # same shape under examples//python/isaaccapture_examples//. + base = Path(__file__).resolve().parents[5] candidates = [ base / "plugins", base / "install" / "plugins", @@ -70,7 +72,9 @@ def _resolve_output(path_arg: str | None) -> Path: if path_arg: path = Path(path_arg) else: - out_dir = Path(__file__).resolve().parent / "recordings" + # CWD-relative, matching mcap_record_replay: anchoring to __file__ would + # write inside the package, and into site-packages once installed. + out_dir = Path.cwd() / "recordings" path = out_dir / f"noitom_full_body_{datetime.now():%Y%m%d_%H%M%S}.mcap" path.parent.mkdir(parents=True, exist_ok=True) return path diff --git a/src/plugins/noitom_mocap/README.md b/src/plugins/noitom_mocap/README.md index 8c3e9dd4b1..d564ac4f78 100644 --- a/src/plugins/noitom_mocap/README.md +++ b/src/plugins/noitom_mocap/README.md @@ -60,7 +60,8 @@ Record through the Noitom example wrapper and replay with the shared MCAP script: ```bash -uv run python examples/noitom/record_noitom_full_body.py \ +uv pip install -e ./examples/noitom +python -m isaaccapture_examples.noitom.record_noitom_full_body \ 10 examples/noitom/recordings/noitom_full_body.mcap uv pip install -e ./examples/mcap_record_replay From 416ab562f8fefb949018e0f18c0264edd356dddf Mon Sep 17 00:00:00 2001 From: Jiwen Cai Date: Fri, 28 Aug 2026 04:47:04 +0000 Subject: [PATCH 2/6] examples(oglo_tactile): package under isaaccapture_examples Package the OGLO examples so sibling imports work outside the source directory. Part of NVIDIA/IsaacCapture#985. Signed-off-by: Jiwen Cai Signed-off-by: Ivan Yang --- examples/oglo_tactile/README.md | 10 +++--- examples/oglo_tactile/pyproject.toml | 32 +++++++++++++++++++ .../oglo_tactile/__init__.py | 2 ++ .../oglo_tactile}/oglo_heatmap.py | 0 .../oglo_tactile}/oglo_teleop_record.py | 8 +++-- 5 files changed, 45 insertions(+), 7 deletions(-) create mode 100644 examples/oglo_tactile/pyproject.toml create mode 100644 examples/oglo_tactile/python/isaaccapture_examples/oglo_tactile/__init__.py rename examples/oglo_tactile/{ => python/isaaccapture_examples/oglo_tactile}/oglo_heatmap.py (100%) rename examples/oglo_tactile/{ => python/isaaccapture_examples/oglo_tactile}/oglo_teleop_record.py (97%) diff --git a/examples/oglo_tactile/README.md b/examples/oglo_tactile/README.md index 6779aa2214..89a104c8cf 100644 --- a/examples/oglo_tactile/README.md +++ b/examples/oglo_tactile/README.md @@ -31,10 +31,11 @@ session, records, and draws the overlay). `oglo_heatmap.py` is the renderer. ```bash sudo apt install libdbus-1-dev # BlueZ for the BLE plugin sudo apt install pkg-config glslang-tools libvulkan-dev # BUILD_VIZ=ON below - pip install pillow numpy # heatmap renderer - pip install cupy-cuda12x # headset overlay GPU upload (match your CUDA) + uv pip install -e ./examples/oglo_tactile # heatmap renderer + deps + uv pip install -e './examples/oglo_tactile[gpu]' # adds the CUDA overlay upload ``` - `cupy` is only needed for the in-headset overlay; recording works without it. + The `gpu` extra is only needed for the in-headset overlay; recording works + without it. Match your CUDA major version if you edit the pin. 2. **Build** (plugin + viz + python wheel) @@ -76,8 +77,7 @@ python -m isaaccapture.cloudxr.service start --accept-eula # flag needed on fi cd ~/Documents/IsaacTeleop source scripts/setup_cloudxr_env.sh source ~/.cloudxr/run/cloudxr.env # path printed by step above -cd examples/oglo_tactile -python oglo_teleop_record.py +python -m isaaccapture_examples.oglo_tactile.oglo_teleop_record ``` The script: connects both gloves over BLE, starts recording to `oglo_teleop_.mcap`, and shows the **two-hand tactile heatmap** in the diff --git a/examples/oglo_tactile/pyproject.toml b/examples/oglo_tactile/pyproject.toml new file mode 100644 index 0000000000..d2a9ddd0ad --- /dev/null +++ b/examples/oglo_tactile/pyproject.toml @@ -0,0 +1,32 @@ +# SPDX-FileCopyrightText: Copyright (c) 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-oglo-tactile" +version = "0.0.0" # Internal example - not versioned +description = "OGLO tactile glove: heatmap renderer and teleop recording" +requires-python = ">=3.11,<3.14" +dependencies = [ + "isaaccapture[cloudxr]", + "numpy>=1.23.0", + "pillow", +] + +[project.optional-dependencies] +# The in-headset overlay uploads the heatmap through CUDA; match your CUDA +# major version. Without it the renderer still writes PNGs. +gpu = ["cupy-cuda12x"] + +# `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 `oglo_tactile/`. +[tool.hatch.build.targets.wheel] +only-include = ["python/isaaccapture_examples/oglo_tactile"] +sources = ["python"] diff --git a/examples/oglo_tactile/python/isaaccapture_examples/oglo_tactile/__init__.py b/examples/oglo_tactile/python/isaaccapture_examples/oglo_tactile/__init__.py new file mode 100644 index 0000000000..52a7a9daf0 --- /dev/null +++ b/examples/oglo_tactile/python/isaaccapture_examples/oglo_tactile/__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/oglo_tactile/oglo_heatmap.py b/examples/oglo_tactile/python/isaaccapture_examples/oglo_tactile/oglo_heatmap.py similarity index 100% rename from examples/oglo_tactile/oglo_heatmap.py rename to examples/oglo_tactile/python/isaaccapture_examples/oglo_tactile/oglo_heatmap.py diff --git a/examples/oglo_tactile/oglo_teleop_record.py b/examples/oglo_tactile/python/isaaccapture_examples/oglo_tactile/oglo_teleop_record.py similarity index 97% rename from examples/oglo_tactile/oglo_teleop_record.py rename to examples/oglo_tactile/python/isaaccapture_examples/oglo_tactile/oglo_teleop_record.py index 8f34c15710..035dfb680c 100755 --- a/examples/oglo_tactile/oglo_teleop_record.py +++ b/examples/oglo_tactile/python/isaaccapture_examples/oglo_tactile/oglo_teleop_record.py @@ -36,7 +36,7 @@ import isaaccapture.deviceio as deviceio import isaaccapture.viz as viz -from oglo_heatmap import NUM_TAXELS, Normalizer, TactileHeatmapRenderer +from .oglo_heatmap import NUM_TAXELS, Normalizer, TactileHeatmapRenderer OVERLAY_W, OVERLAY_H = 1024, 512 COLLECTION_PREFIX = "oglo" @@ -50,8 +50,12 @@ def _on_signal(signum: int, frame: FrameType | None) -> None: def _default_plugin_bin() -> str: + # Only meaningful from a source checkout: examples/oglo_tactile/python/ + # isaaccapture_examples/oglo_tactile/ is five levels below the repo root. An + # installed copy lands in site-packages, where this finds nothing and the + # PATH fallback below takes over. here = Path(__file__).resolve() - root = here.parents[2] # repo root + root = here.parents[5] for cand in ( root / "build/src/plugins/oglo_tactile/oglo_tactile_plugin", root / "install/plugins/oglo_tactile/oglo_tactile_plugin", From d6e6c2de5d5d093ae206e5c05b13452900282361 Mon Sep 17 00:00:00 2001 From: Ivan Yang Date: Tue, 27 Oct 2026 16:34:59 -0700 Subject: [PATCH 3/6] examples(noitom, oglo_tactile): fix run commands the move left behind Point the oglo_tactile module docstrings at `python -m`, and record the Noitom example to the repo-root `recordings/` the recorder now defaults to, which .gitignore already covers. Drop the install-prefix case from the Noitom plugin-path comment, since CMake does not install this example, and the README's note on the old PYTHONPATH prefix. Part of NVIDIA/IsaacCapture#985. Signed-off-by: Ivan Yang --- .gitignore | 4 ++-- examples/noitom/README.md | 7 +++---- .../python/isaaccapture_examples/noitom/noitom_tasks.py | 3 +-- .../noitom/record_noitom_full_body.py | 3 +-- .../isaaccapture_examples/oglo_tactile/oglo_heatmap.py | 2 +- .../oglo_tactile/oglo_teleop_record.py | 2 +- src/plugins/noitom_mocap/README.md | 4 ++-- 7 files changed, 11 insertions(+), 14 deletions(-) diff --git a/.gitignore b/.gitignore index 1355488147..ac4693b66e 100644 --- a/.gitignore +++ b/.gitignore @@ -63,8 +63,8 @@ project.spdx # MuJoCo writes this to the working directory, not next to the model. MUJOCO_LOG.TXT -# examples/mcap_record_replay writes here by default (Path.cwd() / "recordings"), -# and its README has you run from the repo root. +# examples/mcap_record_replay and examples/noitom write here by default +# (Path.cwd() / "recordings"), and their READMEs have you run from the repo root. /recordings/ # Runtime device files injected by MCP server infrastructure diff --git a/examples/noitom/README.md b/examples/noitom/README.md index 81c0457150..3e50b72433 100644 --- a/examples/noitom/README.md +++ b/examples/noitom/README.md @@ -50,7 +50,7 @@ Install the example once, then record the Noitom full-body stream: ```bash uv pip install -e ./examples/noitom python -m isaaccapture_examples.noitom.record_noitom_full_body \ - 10 examples/noitom/recordings/noitom_full_body.mcap + 10 recordings/noitom_full_body.mcap ``` The resulting MCAP uses the standard `core.FullBodyPoseRecord` schema and @@ -61,7 +61,7 @@ the standard `full_body` channel, so the generic replay script can play it back: ```bash uv pip install -e ./examples/mcap_record_replay python -m isaaccapture_examples.mcap_record_replay.replay_full_body \ - examples/noitom/recordings/noitom_full_body.mcap + recordings/noitom_full_body.mcap ``` ## Example 2: Teleop @@ -72,8 +72,7 @@ Run Isaac Lab with the external task registration callback. The task launches `noitom_mocap_plugin` through IsaacTeleop's plugin manager by default. The callback runs inside Isaac Lab's own interpreter, so install the example -into *that* environment rather than a venv of your own — this replaces the -`PYTHONPATH=` prefix these commands used to carry: +into *that* environment rather than a venv of your own: ```bash cd ~/dependence/IsaacLab3-0 diff --git a/examples/noitom/python/isaaccapture_examples/noitom/noitom_tasks.py b/examples/noitom/python/isaaccapture_examples/noitom/noitom_tasks.py index 0eacf837f6..a52fc4029a 100644 --- a/examples/noitom/python/isaaccapture_examples/noitom/noitom_tasks.py +++ b/examples/noitom/python/isaaccapture_examples/noitom/noitom_tasks.py @@ -168,8 +168,7 @@ def _noitom_settings_from_env() -> NoitomG1Settings: def _plugin_search_paths() -> list[Path]: - # Six levels up is the repo root -- or the install prefix, which has the - # same shape under examples//python/isaaccapture_examples//. + # Six levels up is the repo root. base = Path(__file__).resolve().parents[5] candidates = [ base / "plugins", diff --git a/examples/noitom/python/isaaccapture_examples/noitom/record_noitom_full_body.py b/examples/noitom/python/isaaccapture_examples/noitom/record_noitom_full_body.py index d1cf959149..ca74e20a78 100644 --- a/examples/noitom/python/isaaccapture_examples/noitom/record_noitom_full_body.py +++ b/examples/noitom/python/isaaccapture_examples/noitom/record_noitom_full_body.py @@ -37,8 +37,7 @@ def _plugin_search_paths() -> list[Path]: - # Six levels up is the repo root -- or the install prefix, which has the - # same shape under examples//python/isaaccapture_examples//. + # Six levels up is the repo root. base = Path(__file__).resolve().parents[5] candidates = [ base / "plugins", diff --git a/examples/oglo_tactile/python/isaaccapture_examples/oglo_tactile/oglo_heatmap.py b/examples/oglo_tactile/python/isaaccapture_examples/oglo_tactile/oglo_heatmap.py index be68668312..704c8297e1 100644 --- a/examples/oglo_tactile/python/isaaccapture_examples/oglo_tactile/oglo_heatmap.py +++ b/examples/oglo_tactile/python/isaaccapture_examples/oglo_tactile/oglo_heatmap.py @@ -12,7 +12,7 @@ Row 0 is the fingertip (distal); rows are flipped so the fingertip draws at the top. Standalone preview (writes a PNG you can open): - python oglo_heatmap.py + python -m isaaccapture_examples.oglo_tactile.oglo_heatmap """ from __future__ import annotations diff --git a/examples/oglo_tactile/python/isaaccapture_examples/oglo_tactile/oglo_teleop_record.py b/examples/oglo_tactile/python/isaaccapture_examples/oglo_tactile/oglo_teleop_record.py index 035dfb680c..f7e21e9fc4 100755 --- a/examples/oglo_tactile/python/isaaccapture_examples/oglo_tactile/oglo_teleop_record.py +++ b/examples/oglo_tactile/python/isaaccapture_examples/oglo_tactile/oglo_teleop_record.py @@ -16,7 +16,7 @@ the Python wheel installed. ``cupy`` is required for the headset overlay (recording still works without it). - python oglo_teleop_record.py --plugin-bin /oglo_tactile_plugin + python -m isaaccapture_examples.oglo_tactile.oglo_teleop_record --plugin-bin /oglo_tactile_plugin """ from __future__ import annotations diff --git a/src/plugins/noitom_mocap/README.md b/src/plugins/noitom_mocap/README.md index d564ac4f78..72151e0e16 100644 --- a/src/plugins/noitom_mocap/README.md +++ b/src/plugins/noitom_mocap/README.md @@ -62,9 +62,9 @@ script: ```bash uv pip install -e ./examples/noitom python -m isaaccapture_examples.noitom.record_noitom_full_body \ - 10 examples/noitom/recordings/noitom_full_body.mcap + 10 recordings/noitom_full_body.mcap uv pip install -e ./examples/mcap_record_replay python -m isaaccapture_examples.mcap_record_replay.replay_full_body \ - examples/noitom/recordings/noitom_full_body.mcap + recordings/noitom_full_body.mcap ``` From 89f451502717eabe6e6d538884244754208beb08 Mon Sep 17 00:00:00 2001 From: Ivan Yang Date: Tue, 27 Oct 2026 17:59:11 -0700 Subject: [PATCH 4/6] examples(noitom, oglo_tactile): fix the Isaac Lab entry point and dependencies - Register the Noitom env config under the package's module name; the bare `noitom_tasks` only resolved with the example on PYTHONPATH. - Declare scipy and the cloudxr extra for the Noitom example, and say why isaaclab and gymnasium stay out. - Search for plugins only under install/plugins, and say in the comment and the not-found errors that this needs a checkout or an editable install. - Drop the OGLO `gpu` extra: isaaccapture[cloudxr] already brings CuPy and Pillow on Linux. Install the example after the build, from the repo root, from the local wheels. Part of NVIDIA/IsaacCapture#985. Signed-off-by: Ivan Yang --- examples/noitom/pyproject.toml | 8 ++++---- .../isaaccapture_examples/noitom/noitom_tasks.py | 16 +++++++--------- .../noitom/record_noitom_full_body.py | 16 +++++++--------- examples/oglo_tactile/README.md | 11 ++++------- examples/oglo_tactile/pyproject.toml | 5 ----- 5 files changed, 22 insertions(+), 34 deletions(-) diff --git a/examples/noitom/pyproject.toml b/examples/noitom/pyproject.toml index 881601cffc..b74ec39629 100644 --- a/examples/noitom/pyproject.toml +++ b/examples/noitom/pyproject.toml @@ -12,12 +12,12 @@ name = "isaaccapture-examples-noitom" version = "0.0.0" # Internal example - not versioned description = "Noitom mocap suit examples: retargeting, Isaac Lab task, MCAP recording" requires-python = ">=3.11,<3.14" -# isaaclab and gymnasium are deliberately absent: noitom_tasks.py runs inside an -# Isaac Lab environment that already provides them, and neither installs from -# PyPI. Install this example into that interpreter, not a fresh venv. +# isaaclab and gymnasium are deliberately absent: only noitom_tasks imports them, +# and it runs inside Isaac Lab's own interpreter, which provides them. dependencies = [ - "isaaccapture", + "isaaccapture[cloudxr]", "numpy>=1.23.0", + "scipy", ] # `isaaccapture_examples` is a PEP 420 namespace shared by every example dist and diff --git a/examples/noitom/python/isaaccapture_examples/noitom/noitom_tasks.py b/examples/noitom/python/isaaccapture_examples/noitom/noitom_tasks.py index a52fc4029a..691964ece0 100644 --- a/examples/noitom/python/isaaccapture_examples/noitom/noitom_tasks.py +++ b/examples/noitom/python/isaaccapture_examples/noitom/noitom_tasks.py @@ -168,13 +168,10 @@ def _noitom_settings_from_env() -> NoitomG1Settings: def _plugin_search_paths() -> list[Path]: - # Six levels up is the repo root. - base = Path(__file__).resolve().parents[5] - candidates = [ - base / "plugins", - base / "install" / "plugins", - ] - return [path for path in candidates if path.exists()] + # Six levels up is the repo root from a checkout or an editable install; a + # regular wheel install puts this file in site-packages and finds nothing. + plugins = Path(__file__).resolve().parents[5] / "install" / "plugins" + return [plugins] if plugins.exists() else [] def _noitom_plugin_configs(settings: NoitomG1Settings) -> list[PluginConfig]: @@ -183,7 +180,8 @@ def _noitom_plugin_configs(settings: NoitomG1Settings) -> list[PluginConfig]: search_paths = _plugin_search_paths() if not search_paths: raise RuntimeError( - "Noitom plugin directory not found. Run `cmake --install build` " + "Noitom plugin directory not found. Install this example editable " + "(`pip install -e`) from a checkout, run `cmake --install build` there, " "or set NOITOM_MOCAP_AUTO_LAUNCH=0 for a manually started plugin." ) return [ @@ -341,7 +339,7 @@ def register_tasks() -> list[str]: id=TASK_ID, entry_point="isaaclab.envs:ManagerBasedRLEnv", kwargs={ - "env_cfg_entry_point": ("noitom_tasks:NoitomLocomanipulationG1EnvCfg"), + "env_cfg_entry_point": f"{__name__}:NoitomLocomanipulationG1EnvCfg", }, disable_env_checker=True, ) diff --git a/examples/noitom/python/isaaccapture_examples/noitom/record_noitom_full_body.py b/examples/noitom/python/isaaccapture_examples/noitom/record_noitom_full_body.py index ca74e20a78..f59953e905 100644 --- a/examples/noitom/python/isaaccapture_examples/noitom/record_noitom_full_body.py +++ b/examples/noitom/python/isaaccapture_examples/noitom/record_noitom_full_body.py @@ -37,13 +37,10 @@ def _plugin_search_paths() -> list[Path]: - # Six levels up is the repo root. - base = Path(__file__).resolve().parents[5] - candidates = [ - base / "plugins", - base / "install" / "plugins", - ] - return [path for path in candidates if path.exists()] + # Six levels up is the repo root from a checkout or an editable install; a + # regular wheel install puts this file in site-packages and finds nothing. + plugins = Path(__file__).resolve().parents[5] / "install" / "plugins" + return [plugins] if plugins.exists() else [] def _build_pipeline(collection_id: str, max_flatbuffer_size: int) -> OutputCombiner: @@ -103,8 +100,9 @@ def main(argv: list[str]) -> int: search_paths = _plugin_search_paths() if not search_paths: sys.exit( - "[record] error: no installed plugin directory found. " - "Run `cmake --install build` or pass `--no-plugin` for a manually " + "[record] error: no installed plugin directory found. Install " + "this example editable (`uv pip install -e ./examples/noitom`), run " + "`cmake --install build`, or pass `--no-plugin` for a manually " "started plugin." ) plugins.append( diff --git a/examples/oglo_tactile/README.md b/examples/oglo_tactile/README.md index 89a104c8cf..b48025c63c 100644 --- a/examples/oglo_tactile/README.md +++ b/examples/oglo_tactile/README.md @@ -31,28 +31,25 @@ session, records, and draws the overlay). `oglo_heatmap.py` is the renderer. ```bash sudo apt install libdbus-1-dev # BlueZ for the BLE plugin sudo apt install pkg-config glslang-tools libvulkan-dev # BUILD_VIZ=ON below - uv pip install -e ./examples/oglo_tactile # heatmap renderer + deps - uv pip install -e './examples/oglo_tactile[gpu]' # adds the CUDA overlay upload ``` - The `gpu` extra is only needed for the in-headset overlay; recording works - without it. Match your CUDA major version if you edit the pin. -2. **Build** (plugin + viz + python wheel) +2. **Build and install** (plugin + viz + python wheel + this example) ```bash cd IsaacTeleop cmake -B build -DBUILD_PLUGIN_OGLO=ON -DBUILD_VIZ=ON cmake --build build --parallel cmake --install build # builds the isaaccapture python wheel/package + uv pip install --find-links=install/wheels "isaaccapture[cloudxr]" + uv pip install -e ./examples/oglo_tactile ctest --test-dir build -R oglo_packet_parser --output-on-failure # parser sanity ``` The BLE backend (BlueZ via libdbus, permissive) connects out of the box. + CuPy for the in-headset overlay comes with `isaaccapture[cloudxr]`. 3. **Gloves**: confirm both run schema-5 firmware, are charged, and advertise `OGLO LEFT` / `OGLO RIGHT` (check with `bluetoothctl scan on`). -4. **CloudXR**: install per the IsaacTeleop Quick Start (`pip install 'isaaccapture[cloudxr]'`). - --- ## B. Demo scenario (run in order) diff --git a/examples/oglo_tactile/pyproject.toml b/examples/oglo_tactile/pyproject.toml index d2a9ddd0ad..3d683ca034 100644 --- a/examples/oglo_tactile/pyproject.toml +++ b/examples/oglo_tactile/pyproject.toml @@ -18,11 +18,6 @@ dependencies = [ "pillow", ] -[project.optional-dependencies] -# The in-headset overlay uploads the heatmap through CUDA; match your CUDA -# major version. Without it the renderer still writes PNGs. -gpu = ["cupy-cuda12x"] - # `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 From 3ee068f907ec2e5ad1e2da258d70f969bf847e40 Mon Sep 17 00:00:00 2001 From: Ivan Yang Date: Tue, 27 Oct 2026 18:15:41 -0700 Subject: [PATCH 5/6] examples(noitom, oglo_tactile): install into Isaac Lab 3.0, align outputs - Noitom README: build for Python 3.12 and install the local isaaccapture wheel by version, then the example, into Isaac Lab's interpreter with no venv active. Say this replaces Isaac Lab's isaacteleop 1.4 with the compatibility shim, and name Isaac Lab 3.0.0 as the minimum. - Import LocomanipulationG1EnvCfg from isaaclab_tasks.contrib, where Isaac Lab 3.0 moved it. - OGLO: write the default MCAP and the heatmap preview under recordings/, document the preview command, and name the module in the flags heading. - Use placeholders for the checkout directories, and point the pyproject packaging comments at examples/README.md. Part of NVIDIA/IsaacCapture#985. Signed-off-by: Ivan Yang --- .gitignore | 5 +-- examples/noitom/README.md | 35 ++++++++++++++----- examples/noitom/pyproject.toml | 6 ++-- .../noitom/noitom_tasks.py | 2 +- examples/oglo_tactile/README.md | 21 +++++++---- examples/oglo_tactile/pyproject.toml | 6 ++-- .../oglo_tactile/oglo_heatmap.py | 4 ++- .../oglo_tactile/oglo_teleop_record.py | 15 +++++--- 8 files changed, 61 insertions(+), 33 deletions(-) diff --git a/.gitignore b/.gitignore index ac4693b66e..123dda2cda 100644 --- a/.gitignore +++ b/.gitignore @@ -63,8 +63,9 @@ project.spdx # MuJoCo writes this to the working directory, not next to the model. MUJOCO_LOG.TXT -# examples/mcap_record_replay and examples/noitom write here by default -# (Path.cwd() / "recordings"), and their READMEs have you run from the repo root. +# examples/mcap_record_replay, examples/noitom and examples/oglo_tactile write here +# by default (Path.cwd() / "recordings"), and their READMEs have you run from the +# repo root. /recordings/ # Runtime device files injected by MCP server infrastructure diff --git a/examples/noitom/README.md b/examples/noitom/README.md index 3e50b72433..832ad3e341 100644 --- a/examples/noitom/README.md +++ b/examples/noitom/README.md @@ -24,12 +24,15 @@ The Noitom SDK is not vendored in this repository. Build the optional plugin when you need Noitom support: ```bash -cmake -B build -DBUILD_PLUGIN_NOITOM_MOCAP=ON +cmake -B build -DBUILD_PLUGIN_NOITOM_MOCAP=ON -DISAAC_TELEOP_PYTHON_VERSION=3.12 cmake --build build --target python_package noitom_mocap_plugin --parallel cmake --install build uv pip install --find-links=install/wheels "isaaccapture[cloudxr]" ``` +Python 3.12 matches Isaac Lab 3.0, which Example 2 needs. The wheel installs +only on the Python it was built for, so use a 3.12 venv for Example 1 too. + Run `cmake --install build` again whenever `plugin.yaml` changes so the updated configuration is copied to `install/plugins/noitom_mocap/plugin.yaml`. @@ -68,19 +71,33 @@ python -m isaaccapture_examples.mcap_record_replay.replay_full_body \ ![Noitom G1 live teleoperation](assets/teleop.gif) -Run Isaac Lab with the external task registration callback. The task launches -`noitom_mocap_plugin` through IsaacTeleop's plugin manager by default. +Run Isaac Lab 3.0.0 or later with the external task registration callback. +The task launches `noitom_mocap_plugin` through IsaacTeleop's plugin manager by +default. Below, `` is your IsaacCapture checkout and `` your +Isaac Lab checkout. -The callback runs inside Isaac Lab's own interpreter, so install the example -into *that* environment rather than a venv of your own: +The callback runs inside Isaac Lab's own interpreter, so install isaaccapture +and this example into *that* environment rather than a venv of your own. Build +with `-DISAAC_TELEOP_PYTHON_VERSION=3.12` as above: Isaac Lab 3.0 runs Python +3.12, and pip skips a wheel built for another version. Deactivate any venv +first, because `isaaclab.sh -p` uses `$VIRTUAL_ENV` when it is set. `X.Y` is +your build's version, the first two numbers of the repository's `VERSION` file. ```bash -cd ~/dependence/IsaacLab3-0 -./isaaclab.sh -p -m pip install -e ~/IsaacTeleop/examples/noitom +deactivate # only if a venv is active +cd +./isaaclab.sh -p -m pip install "isaaccapture[cloudxr]==X.Y+local" \ + --find-links /install/wheels +./isaaclab.sh -p -m pip install -e /examples/noitom ``` +This replaces the `isaacteleop` 1.4 that Isaac Lab installs with the `1!X.Y` +compatibility shim from your build, which maps `isaacteleop` to +`isaaccapture`. Isaac Lab pins `isaacteleop~=1.4.145`, so pip may warn about +that conflict. + ```bash -cd ~/dependence/IsaacLab3-0 +cd ./isaaclab.sh -p scripts/environments/teleoperation/teleop_se3_agent.py \ --task Isaac-PickPlace-Locomanipulation-G1-Noitom-Abs-v0 \ --visualizer kit \ @@ -92,7 +109,7 @@ For advanced manual plugin control, start the plugin yourself and disable auto-launch in the Isaac Lab terminal: ```bash -./install/plugins/noitom_mocap/noitom_mocap_plugin +/install/plugins/noitom_mocap/noitom_mocap_plugin NOITOM_MOCAP_AUTO_LAUNCH=0 \ ./isaaclab.sh -p scripts/environments/teleoperation/teleop_se3_agent.py \ diff --git a/examples/noitom/pyproject.toml b/examples/noitom/pyproject.toml index b74ec39629..5400dcb080 100644 --- a/examples/noitom/pyproject.toml +++ b/examples/noitom/pyproject.toml @@ -20,10 +20,8 @@ dependencies = [ "scipy", ] -# `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 `noitom/`. +# `only-include` + `sources`, not `packages`, and no __init__.py above +# noitom -- see the packaging rules in examples/README.md. [tool.hatch.build.targets.wheel] only-include = ["python/isaaccapture_examples/noitom"] sources = ["python"] diff --git a/examples/noitom/python/isaaccapture_examples/noitom/noitom_tasks.py b/examples/noitom/python/isaaccapture_examples/noitom/noitom_tasks.py index 691964ece0..15cdc54328 100644 --- a/examples/noitom/python/isaaccapture_examples/noitom/noitom_tasks.py +++ b/examples/noitom/python/isaaccapture_examples/noitom/noitom_tasks.py @@ -15,7 +15,7 @@ from gymnasium.envs.registration import registry from isaaclab.utils.configclass import configclass -from isaaclab_tasks.manager_based.locomanipulation.pick_place.locomanipulation_g1_env_cfg import ( +from isaaclab_tasks.contrib.locomanip_pick_place.locomanipulation_g1_env_cfg import ( LocomanipulationG1EnvCfg, ) from isaaccapture.schema import ( diff --git a/examples/oglo_tactile/README.md b/examples/oglo_tactile/README.md index b48025c63c..56620e9051 100644 --- a/examples/oglo_tactile/README.md +++ b/examples/oglo_tactile/README.md @@ -36,7 +36,7 @@ session, records, and draws the overlay). `oglo_heatmap.py` is the renderer. 2. **Build and install** (plugin + viz + python wheel + this example) ```bash - cd IsaacTeleop + cd # your IsaacCapture checkout cmake -B build -DBUILD_PLUGIN_OGLO=ON -DBUILD_VIZ=ON cmake --build build --parallel cmake --install build # builds the isaaccapture python wheel/package @@ -47,6 +47,13 @@ session, records, and draws the overlay). `oglo_heatmap.py` is the renderer. The BLE backend (BlueZ via libdbus, permissive) connects out of the box. CuPy for the in-headset overlay comes with `isaaccapture[cloudxr]`. + To check the heatmap renderer without gloves or a headset, render a synthetic + frame to `recordings/oglo_heatmap_demo.png`: + + ```bash + python -m isaaccapture_examples.oglo_tactile.oglo_heatmap + ``` + 3. **Gloves**: confirm both run schema-5 firmware, are charged, and advertise `OGLO LEFT` / `OGLO RIGHT` (check with `bluetoothctl scan on`). @@ -56,7 +63,7 @@ session, records, and draws the overlay). `oglo_heatmap.py` is the renderer. **Start the CloudXR runtime** — a background service, so this shell stays free ```bash -cd ~/Documents/IsaacTeleop +cd source scripts/setup_cloudxr_env.sh python -m isaaccapture.cloudxr.service start --accept-eula # flag needed on first run only # Note the printed web-client URL, e.g. https://:48322/ @@ -71,13 +78,13 @@ python -m isaaccapture.cloudxr.service start --accept-eula # flag needed on fi **Gloves + recording + overlay** ```bash -cd ~/Documents/IsaacTeleop +cd source scripts/setup_cloudxr_env.sh source ~/.cloudxr/run/cloudxr.env # path printed by step above python -m isaaccapture_examples.oglo_tactile.oglo_teleop_record ``` The script: connects both gloves over BLE, starts recording to -`oglo_teleop_.mcap`, and shows the **two-hand tactile heatmap** in the +`recordings/oglo_teleop_.mcap`, and shows the **two-hand tactile heatmap** in the headset. **Keep hands relaxed for ~1 s** at start so the baseline tares. Press a glove → the matching finger/taxel lights up (YlOrRd) on the Quest screen. @@ -90,7 +97,7 @@ Press a glove → the matching finger/taxel lights up (YlOrRd) on the Quest scre ## C. Verify the recording ```bash -mcap info oglo_teleop_*.mcap +mcap info recordings/oglo_teleop_*.mcap # channels: hands/{left,right}_hand, head/head, oglo_{left,right}/oglo(+_tracked) ``` All streams share `sample_time_local_common_clock`, so Quest pose and OGLO @@ -98,12 +105,12 @@ tactile align in time. --- -## Useful flags (`oglo_teleop_record.py`) +## Useful flags (`isaaccapture_examples.oglo_tactile.oglo_teleop_record`) | Flag | Purpose | |------|---------| | `--plugin-bin PATH` | Path to `oglo_tactile_plugin` (auto-detected under `build/`). | -| `--mcap PATH` | Output file (default timestamped). | +| `--mcap PATH` | Output file (default `recordings/oglo_teleop_.mcap`). | | `--duration S` | Auto-stop after S seconds (0 = until Ctrl+C). | | `--raw` | Raw-ADC heatmap (no baseline subtraction). | | `--no-overlay` | Record only (skip headset heatmap). | diff --git a/examples/oglo_tactile/pyproject.toml b/examples/oglo_tactile/pyproject.toml index 3d683ca034..66df54bb70 100644 --- a/examples/oglo_tactile/pyproject.toml +++ b/examples/oglo_tactile/pyproject.toml @@ -18,10 +18,8 @@ dependencies = [ "pillow", ] -# `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 `oglo_tactile/`. +# `only-include` + `sources`, not `packages`, and no __init__.py above +# oglo_tactile -- see the packaging rules in examples/README.md. [tool.hatch.build.targets.wheel] only-include = ["python/isaaccapture_examples/oglo_tactile"] sources = ["python"] diff --git a/examples/oglo_tactile/python/isaaccapture_examples/oglo_tactile/oglo_heatmap.py b/examples/oglo_tactile/python/isaaccapture_examples/oglo_tactile/oglo_heatmap.py index 704c8297e1..600b9a7275 100644 --- a/examples/oglo_tactile/python/isaaccapture_examples/oglo_tactile/oglo_heatmap.py +++ b/examples/oglo_tactile/python/isaaccapture_examples/oglo_tactile/oglo_heatmap.py @@ -18,6 +18,7 @@ from __future__ import annotations from dataclasses import dataclass, field +from pathlib import Path import numpy as np @@ -299,7 +300,8 @@ def _demo() -> None: renderer = TactileHeatmapRenderer(width=1024, height=512) frame = renderer.render(norm.normalize(left), norm.normalize(right)) assert frame.shape == (512, 1024, 4) and frame.dtype == np.uint8 - out = "oglo_heatmap_demo.png" + out = Path.cwd() / "recordings" / "oglo_heatmap_demo.png" + out.parent.mkdir(parents=True, exist_ok=True) Image.fromarray(frame, "RGBA").save(out) nonzero = int((frame[..., :3] > 0).any(axis=-1).sum()) print(f"wrote {out} shape={frame.shape} non-background px={nonzero}") diff --git a/examples/oglo_tactile/python/isaaccapture_examples/oglo_tactile/oglo_teleop_record.py b/examples/oglo_tactile/python/isaaccapture_examples/oglo_tactile/oglo_teleop_record.py index f7e21e9fc4..88538984cc 100755 --- a/examples/oglo_tactile/python/isaaccapture_examples/oglo_tactile/oglo_teleop_record.py +++ b/examples/oglo_tactile/python/isaaccapture_examples/oglo_tactile/oglo_teleop_record.py @@ -160,7 +160,9 @@ def main(argv: list[str] | None = None) -> int: help="path to oglo_tactile_plugin", ) parser.add_argument( - "--mcap", default=None, help="output MCAP path (default: timestamped)" + "--mcap", + default=None, + help="output MCAP path (default: recordings/oglo_teleop_.mcap)", ) parser.add_argument( "--duration", @@ -241,9 +243,12 @@ def main(argv: list[str] | None = None) -> int: signal.signal(signal.SIGINT, _on_signal) signal.signal(signal.SIGTERM, _on_signal) - mcap_path = ( - args.mcap or f"oglo_teleop_{datetime.now().strftime('%Y%m%d_%H%M%S')}.mcap" - ) + if args.mcap: + mcap_path = Path(args.mcap) + else: + stamp = datetime.now().strftime("%Y%m%d_%H%M%S") + mcap_path = Path.cwd() / "recordings" / f"oglo_teleop_{stamp}.mcap" + mcap_path.parent.mkdir(parents=True, exist_ok=True) # --- trackers: Quest hand/head + both gloves -------------------------------- hand = deviceio.HandTracker() @@ -300,7 +305,7 @@ def main(argv: list[str] | None = None) -> int: norm_r = Normalizer(raw=args.raw) recording = deviceio.McapRecordingConfig( - mcap_path, + str(mcap_path), [ (hand, "hands"), (head, "head"), From 2a4fff31ca314a8dd29fdd3b12a437b11c37443f Mon Sep 17 00:00:00 2001 From: Ivan Yang Date: Tue, 27 Oct 2026 18:48:40 -0700 Subject: [PATCH 6/6] examples(noitom): build the wheel the install steps use Building only python_package stages the package but produces no wheel, so on a clean build install/wheels has nothing for the pinned isaaccapture install to find. Build python_wheel as well. Part of NVIDIA/IsaacCapture#985. Signed-off-by: Ivan Yang --- examples/noitom/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/noitom/README.md b/examples/noitom/README.md index 832ad3e341..695db9dcc5 100644 --- a/examples/noitom/README.md +++ b/examples/noitom/README.md @@ -25,7 +25,7 @@ when you need Noitom support: ```bash cmake -B build -DBUILD_PLUGIN_NOITOM_MOCAP=ON -DISAAC_TELEOP_PYTHON_VERSION=3.12 -cmake --build build --target python_package noitom_mocap_plugin --parallel +cmake --build build --target python_package python_wheel noitom_mocap_plugin --parallel cmake --install build uv pip install --find-links=install/wheels "isaaccapture[cloudxr]" ```