diff --git a/examples/noitom/README.md b/examples/noitom/README.md index 0dc48edc82..98c40524f5 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 isaacteleop_examples.noitom.record_noitom_full_body \ 10 examples/noitom/recordings/noitom_full_body.mcap ``` @@ -70,14 +71,22 @@ uv run python replay_full_body.py ../../noitom/recordings/noitom_full_body.mcap 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 isaacteleop_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 isaacteleop_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..347d9004aa --- /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 = "isaacteleop-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 = [ + "isaacteleop", + "numpy>=1.23.0", +] + +# `isaacteleop_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/isaacteleop_examples/noitom"] +sources = ["python"] diff --git a/examples/noitom/python/isaacteleop_examples/noitom/__init__.py b/examples/noitom/python/isaacteleop_examples/noitom/__init__.py new file mode 100644 index 0000000000..52a7a9daf0 --- /dev/null +++ b/examples/noitom/python/isaacteleop_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/isaacteleop_examples/noitom/noitom_reference_draw.py similarity index 99% rename from examples/noitom/noitom_reference_draw.py rename to examples/noitom/python/isaacteleop_examples/noitom/noitom_reference_draw.py index 2f6be4b73c..0f785950ed 100644 --- a/examples/noitom/noitom_reference_draw.py +++ b/examples/noitom/python/isaacteleop_examples/noitom/noitom_reference_draw.py @@ -13,7 +13,7 @@ from isaacteleop.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/isaacteleop_examples/noitom/noitom_retargeting.py similarity index 99% rename from examples/noitom/noitom_retargeting.py rename to examples/noitom/python/isaacteleop_examples/noitom/noitom_retargeting.py index 52973038e8..7e2dbcc0c8 100644 --- a/examples/noitom/noitom_retargeting.py +++ b/examples/noitom/python/isaacteleop_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/isaacteleop_examples/noitom/noitom_tasks.py similarity index 99% rename from examples/noitom/noitom_tasks.py rename to examples/noitom/python/isaacteleop_examples/noitom/noitom_tasks.py index 7820f90c0f..5ea8d3e9ea 100644 --- a/examples/noitom/noitom_tasks.py +++ b/examples/noitom/python/isaacteleop_examples/noitom/noitom_tasks.py @@ -37,13 +37,13 @@ from isaacteleop.retargeting_engine.tensor_types import DLDataType, NDArrayType from isaacteleop.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/isaacteleop_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/isaacteleop_examples/noitom/record_noitom_full_body.py similarity index 93% rename from examples/noitom/record_noitom_full_body.py rename to examples/noitom/python/isaacteleop_examples/noitom/record_noitom_full_body.py index dcf3a693ec..058b2a5e71 100644 --- a/examples/noitom/record_noitom_full_body.py +++ b/examples/noitom/python/isaacteleop_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/isaacteleop_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 f6d8cc750a..2e46b0c3d2 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 isaacteleop_examples.noitom.record_noitom_full_body \ 10 examples/noitom/recordings/noitom_full_body.mcap uv pip install -e ./examples/mcap_record_replay