From 9b7afe0d865b4ac3f768650b0f0e65deb5910ea8 Mon Sep 17 00:00:00 2001 From: Jiwen Cai Date: Fri, 28 Aug 2026 04:37:53 +0000 Subject: [PATCH 1/8] examples(mcap_record_replay): package under isaacteleop_examples Nine modules imported `common` as a top-level module, which resolved only because the invoked script's own directory landed on sys.path -- so they broke under `python -m`, broke when copied into another project, and claimed the very generic bare name `common`. Move the tree to the layout in examples/README.md. Twelve co-equal scripts, so no __main__.py; the README maps channel to live/record/replay. Behaviour change forced by the move: recordings default to ./recordings/ relative to the working directory rather than a path derived from __file__, which after the move points inside the package and for an installed copy would have written into site-packages. Replay searches the same directory. Three deliberate viewer fixes. The seven viser viewers bind 0.0.0.0 rather than 127.0.0.1, since they run where the hardware is and get opened from a laptop; --host 127.0.0.1 restores it. Each startup line reports the bind instead of always printing "localhost". And the grid lay in viser's default XY plane, which stands up as a wall once the up direction is +y: it is now an xz ground plane with the camera centred on it, as one helper in common.py rather than seven copies. Part of NVIDIA/IsaacTeleop#985. Signed-off-by: Jiwen Cai --- docs/source/device/trackers.rst | 6 +- docs/source/references/mcap_record_replay.rst | 33 ++++----- examples/mcap_record_replay/CMakeLists.txt | 2 +- examples/mcap_record_replay/README.md | 35 +++++++++ .../{python => }/pyproject.toml | 18 ++++- .../mcap_record_replay/__init__.py | 2 + .../mcap_record_replay}/common.py | 71 ++++++++++++++++++- .../mcap_record_replay}/live_controller.py | 15 ++-- .../mcap_record_replay}/live_full_body.py | 16 ++--- .../mcap_record_replay}/live_hand.py | 14 ++-- .../mcap_record_replay}/record_controller.py | 6 +- .../mcap_record_replay}/record_full_body.py | 6 +- .../mcap_record_replay}/record_hand.py | 6 +- .../mcap_record_replay}/record_se3_vive.py | 4 +- .../mcap_record_replay}/replay_controller.py | 19 +++-- .../mcap_record_replay}/replay_full_body.py | 20 +++--- .../mcap_record_replay}/replay_hand.py | 18 ++--- .../mcap_record_replay}/replay_se3_vive.py | 19 ++--- src/plugins/noitom_mocap/README.md | 6 +- src/plugins/vive_se3_tracker/README.md | 11 ++- 20 files changed, 214 insertions(+), 113 deletions(-) create mode 100644 examples/mcap_record_replay/README.md rename examples/mcap_record_replay/{python => }/pyproject.toml (53%) create mode 100644 examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/__init__.py rename examples/mcap_record_replay/python/{ => isaacteleop_examples/mcap_record_replay}/common.py (86%) rename examples/mcap_record_replay/python/{ => isaacteleop_examples/mcap_record_replay}/live_controller.py (87%) rename examples/mcap_record_replay/python/{ => isaacteleop_examples/mcap_record_replay}/live_full_body.py (85%) rename examples/mcap_record_replay/python/{ => isaacteleop_examples/mcap_record_replay}/live_hand.py (86%) rename examples/mcap_record_replay/python/{ => isaacteleop_examples/mcap_record_replay}/record_controller.py (94%) rename examples/mcap_record_replay/python/{ => isaacteleop_examples/mcap_record_replay}/record_full_body.py (94%) rename examples/mcap_record_replay/python/{ => isaacteleop_examples/mcap_record_replay}/record_hand.py (94%) rename examples/mcap_record_replay/python/{ => isaacteleop_examples/mcap_record_replay}/record_se3_vive.py (97%) rename examples/mcap_record_replay/python/{ => isaacteleop_examples/mcap_record_replay}/replay_controller.py (90%) rename examples/mcap_record_replay/python/{ => isaacteleop_examples/mcap_record_replay}/replay_full_body.py (88%) rename examples/mcap_record_replay/python/{ => isaacteleop_examples/mcap_record_replay}/replay_hand.py (88%) rename examples/mcap_record_replay/python/{ => isaacteleop_examples/mcap_record_replay}/replay_se3_vive.py (94%) diff --git a/docs/source/device/trackers.rst b/docs/source/device/trackers.rst index ba6bbfcfed..a8ccc1c818 100644 --- a/docs/source/device/trackers.rst +++ b/docs/source/device/trackers.rst @@ -260,9 +260,9 @@ reads the PICO ``XR_BD_body_tracking`` extension directly. - :code-file:`examples/schemaio/full_body_printer.cpp` - :code-file:`examples/mcap_record_replay/cpp/record_full_body.cpp` - - :code-file:`examples/mcap_record_replay/python/live_full_body.py` - - :code-file:`examples/mcap_record_replay/python/record_full_body.py` - - :code-file:`examples/mcap_record_replay/python/replay_full_body.py` + - :code-file:`examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/live_full_body.py` + - :code-file:`examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/record_full_body.py` + - :code-file:`examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_full_body.py` .. note:: diff --git a/docs/source/references/mcap_record_replay.rst b/docs/source/references/mcap_record_replay.rst index eb53d36953..a9025911ba 100644 --- a/docs/source/references/mcap_record_replay.rst +++ b/docs/source/references/mcap_record_replay.rst @@ -130,7 +130,7 @@ Runnable Example ---------------- A complete record / replay example lives at -``examples/mcap_record_replay/python/``: +``examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/``: - ``common.py`` — pipeline builders (``build_hand_pipeline()``, ``build_controller_pipeline()``, ``build_full_body_pipeline()``) plus the @@ -177,10 +177,9 @@ From the example directory: .. code-block:: bash - cd examples/mcap_record_replay/python - uv sync - uv run python live_full_body.py --accept-eula - uv run python live_full_body.py --port 8090 --accept-eula # change viser port + uv pip install -e ./examples/mcap_record_replay + python -m isaacteleop_examples.mcap_record_replay.live_full_body --accept-eula + python -m isaacteleop_examples.mcap_record_replay.live_full_body --port 8090 --accept-eula Open the printed URL (default ``http://localhost:8080``) in a browser. The viewers bind every interface, since they run where the headset is and get @@ -193,19 +192,21 @@ From the example directory: .. code-block:: bash - cd examples/mcap_record_replay/python - uv sync - uv run python record_hand.py # 5 s → ../recordings/hands_.mcap - uv run python record_hand.py 10 # record for 10 seconds - uv run python record_hand.py 10 out.mcap # custom output path + uv pip install -e ./examples/mcap_record_replay + R="python -m isaacteleop_examples.mcap_record_replay.record_hand" + $R # 5 s → ./recordings/hands_.mcap + $R 10 # record for 10 seconds + $R 10 out.mcap # custom output path + +Recordings are written to ``./recordings/`` relative to where you run the +command, and the replay scripts look there when given no path. The example never downloads a published wheel — it runs against the -``isaacteleop`` next to it. Above, ``uv sync`` builds it from this checkout; the -first sync compiles the extension modules and takes a few minutes, later ones -reuse the cached build, and the install is editable, so edits under -``src/python/`` need no rebuild. From -``install/examples/mcap_record_replay/python`` (after ``cmake --install``) it -picks up the wheel you just built instead — see +``isaacteleop`` next to it, built from this checkout. The first install +compiles the extension modules and takes a few minutes; later ones reuse the +cached build, and the install is editable, so edits under ``src/python/`` need +no rebuild. From ``install/examples/mcap_record_replay`` (after ``cmake +--install``) it picks up the wheel you just built instead — see :doc:`/getting_started/build_from_source/index`. An active OpenXR runtime / headset must be connected, just like any other diff --git a/examples/mcap_record_replay/CMakeLists.txt b/examples/mcap_record_replay/CMakeLists.txt index 7a977c5e27..d59cf24f12 100644 --- a/examples/mcap_record_replay/CMakeLists.txt +++ b/examples/mcap_record_replay/CMakeLists.txt @@ -7,4 +7,4 @@ cmake_minimum_required(VERSION 3.20) add_subdirectory(cpp) include(${CMAKE_SOURCE_DIR}/cmake/InstallPythonExample.cmake) -install_python_example(DESTINATION examples/mcap_record_replay/python) +install_python_example(DESTINATION examples/mcap_record_replay) diff --git a/examples/mcap_record_replay/README.md b/examples/mcap_record_replay/README.md new file mode 100644 index 0000000000..f77976d153 --- /dev/null +++ b/examples/mcap_record_replay/README.md @@ -0,0 +1,35 @@ + + +# MCAP record / replay + +Record DeviceIO tracking to an MCAP file and replay it into a viser 3D view. +Live viewers are included for watching without recording. + +```bash +uv pip install -e ./examples/mcap_record_replay +python -m isaacteleop_examples.mcap_record_replay.record_hand # 5 s +python -m isaacteleop_examples.mcap_record_replay.replay_hand # newest take +``` + +Recordings are written to `./recordings/` relative to where you run the +command; a replay given no path picks the newest file there. + +The live and replay viewers bind every interface, so a browser on another +machine can reach them at `http://:8080`. Pass `--host 127.0.0.1` to +keep a viewer local. + +| Channel | Live | Record | Replay | +| --- | --- | --- | --- | +| Hands | `live_hand` | `record_hand` | `replay_hand` | +| Controllers | `live_controller` | `record_controller` | `replay_controller` | +| Full body | `live_full_body` | `record_full_body` | `replay_full_body` | +| VIVE SE3 trackers | — | `record_se3_vive` | `replay_se3_vive` | + +`record_*` takes an optional duration in seconds and an optional output path. +Recording needs a live OpenXR runtime; replay needs only the file. + +A C++ recorder lives in `cpp/`. Docs: +`docs/source/references/mcap_record_replay.rst`. diff --git a/examples/mcap_record_replay/python/pyproject.toml b/examples/mcap_record_replay/pyproject.toml similarity index 53% rename from examples/mcap_record_replay/python/pyproject.toml rename to examples/mcap_record_replay/pyproject.toml index 440fe3d8c2..9bed222988 100644 --- a/examples/mcap_record_replay/python/pyproject.toml +++ b/examples/mcap_record_replay/pyproject.toml @@ -1,8 +1,14 @@ # 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] -name = "mcap-record-replay-example" +# The dist name mirrors the import path, so an installed example claims no bare +# top-level name in site-packages. +name = "isaacteleop-examples-mcap-record-replay" version = "0.0.0" # Internal example - not versioned description = "Isaac Teleop MCAP record / replay example with viser visualization" requires-python = ">=3.11,<3.14" @@ -15,8 +21,16 @@ dependencies = [ "viser>=0.2.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 `mcap_record_replay/`. +[tool.hatch.build.targets.wheel] +only-include = ["python/isaacteleop_examples/mcap_record_replay"] +sources = ["python"] + [tool.uv.sources] # Editable, so edits under src/python/ need no rebuild. Dropped from the # installed copy, which has no repo above it. # TODO(#880): nothing runs this example in CI; neither path is covered. -isaacteleop = { path = "../../..", editable = true } +isaacteleop = { path = "../..", editable = true } diff --git a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/__init__.py b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/__init__.py new file mode 100644 index 0000000000..52a7a9daf0 --- /dev/null +++ b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/__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/mcap_record_replay/python/common.py b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/common.py similarity index 86% rename from examples/mcap_record_replay/python/common.py rename to examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/common.py index 835db44bd8..76bf5340bb 100644 --- a/examples/mcap_record_replay/python/common.py +++ b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/common.py @@ -48,6 +48,71 @@ ) +class GroundGrid: + """The ground plane and the default camera, anchored to what is tracked. + + The session asks OpenXR for a stage (floor-relative) space, but a runtime + that cannot supply one falls back to a head-relative origin: y=0 then sits + at eye height and the skeleton hangs below a grid drawn at zero. Following + the lowest tracked joint puts the grid on the floor in either space, and + the camera is framed against that floor rather than against y=0 -- aiming + at a fixed height leaves the subject at the bottom of the viewport in a + head-relative space. + """ + + def __init__(self, server, handle, smoothing: float = 0.05): + self._server = server + self._handle = handle + self._smoothing = smoothing + self._y: float | None = None + + @server.on_client_connect + def _(client) -> None: + self._frame(client) + + def _frame(self, client) -> None: + """Stand back from the floor at eye height, looking at torso height.""" + floor = 0.0 if self._y is None else self._y + client.camera.position = (0.0, floor + 1.5, 2.5) + client.camera.look_at = (0.0, floor + 0.9, 0.0) + + def follow(self, positions: np.ndarray, valid: np.ndarray) -> None: + points = np.asarray(positions, dtype=np.float32)[np.asarray(valid, dtype=bool)] + if points.size == 0: + return + lowest = float(np.min(points[:, 1])) + # Ease toward it: a single mistracked frame should not drop the floor. + first = self._y is None + self._y = lowest if first else self._y + self._smoothing * (lowest - self._y) + self._handle.position = (0.0, self._y, 0.0) + + # Re-aim once, when the floor is first known. Doing it every frame would + # fight the mouse. + if first: + for client in self._server.get_clients().values(): + self._frame(client) + + +def setup_scene(server) -> GroundGrid: + """Up axis, ground grid and a starting camera, shared by every viewer here. + + viser's ``add_grid`` defaults to the XY plane, which stands up as a wall + once the up direction is +y -- it has to be ``xz`` to lie on the ground. + Returns the grid so a caller with tracked joints can keep it on the floor. + """ + server.scene.set_up_direction("+y") + grid = server.scene.add_grid( + name="/grid", + width=6.0, + height=6.0, + plane="xz", + cell_size=0.25, + section_size=1.0, + ) + + return GroundGrid(server, grid) + + _ZERO_POSITIONS = np.zeros((NUM_HAND_JOINTS, 3), dtype=np.float32) @@ -456,7 +521,8 @@ def update(self, state: dict) -> None: class FullBodyViz: """Viser handles for full-body skeleton (joint cloud + skeleton segments).""" - def __init__(self, server: viser.ViserServer): + def __init__(self, server: viser.ViserServer, ground: "GroundGrid | None" = None): + self._ground = ground self.color = np.array(TRACKED_COLOR, dtype=np.float32) zero_pts = np.zeros((len(BODY_JOINT_NAMES), 3), dtype=np.float32) zero_segs = np.zeros((0, 2, 3), dtype=np.float32) @@ -494,3 +560,6 @@ def update(self, positions: np.ndarray | None, valid: np.ndarray | None) -> None segs = _valid_bone_segments(positions, valid_bool) self.bones.points = segs self.bones.colors = np.tile(self.color, (segs.shape[0], 2, 1)) + + if self._ground is not None: + self._ground.follow(positions, valid_bool) diff --git a/examples/mcap_record_replay/python/live_controller.py b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/live_controller.py similarity index 87% rename from examples/mcap_record_replay/python/live_controller.py rename to examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/live_controller.py index 850fe12802..d1c7b998ee 100644 --- a/examples/mcap_record_replay/python/live_controller.py +++ b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/live_controller.py @@ -26,7 +26,8 @@ from isaacteleop.cloudxr import CloudXRLauncher from isaacteleop.teleop_session_manager import TeleopSession, TeleopSessionConfig -from common import ( +from .common import ( + setup_scene, ControllerViz, LEFT_COLOR, RIGHT_COLOR, @@ -39,16 +40,15 @@ def main(argv: list[str]) -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( "--host", - default="0.0.0.0", - help="Viser HTTP bind address (default: 0.0.0.0, all interfaces; pass 127.0.0.1 to keep it local)", + default="127.0.0.1", + help="Viser HTTP bind address (default: 127.0.0.1; pass 0.0.0.0 to expose externally)", ) parser.add_argument("--port", type=int, default=8080, help="Viser HTTP port") CloudXRLauncher.add_launcher_arguments(parser) args = parser.parse_args(argv[1:]) server = viser.ViserServer(host=args.host, port=args.port) - server.scene.set_up_direction("+y") - server.scene.add_grid(name="/grid", width=2.0, height=2.0, cell_size=0.1) + setup_scene(server) config = TeleopSessionConfig( app_name="LiveControllerExample", @@ -63,10 +63,7 @@ def main(argv: list[str]) -> int: with TeleopSession(config) as session: viz_left = ControllerViz(server, "controller_left", LEFT_COLOR) viz_right = ControllerViz(server, "controller_right", RIGHT_COLOR) - print( - f"[live] viser listening on {args.host}:{args.port} " - f"(http://localhost:{args.port})" - ) + print(f"[live] viser running at http://localhost:{args.port}") _last_step_t = time.time() _missed = 0 try: diff --git a/examples/mcap_record_replay/python/live_full_body.py b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/live_full_body.py similarity index 85% rename from examples/mcap_record_replay/python/live_full_body.py rename to examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/live_full_body.py index 883a1f39d9..702ca2fb82 100644 --- a/examples/mcap_record_replay/python/live_full_body.py +++ b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/live_full_body.py @@ -28,23 +28,22 @@ from isaacteleop.retargeting_engine.tensor_types.indices import FullBodyInputIndex from isaacteleop.teleop_session_manager import TeleopSession, TeleopSessionConfig -from common import BODY_JOINT_NAMES, FullBodyViz, build_full_body_pipeline +from .common import BODY_JOINT_NAMES, FullBodyViz, build_full_body_pipeline, setup_scene def main(argv: list[str]) -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( "--host", - default="0.0.0.0", - help="Viser HTTP bind address (default: 0.0.0.0, all interfaces; pass 127.0.0.1 to keep it local)", + default="127.0.0.1", + help="Viser HTTP bind address (default: 127.0.0.1; pass 0.0.0.0 to expose externally)", ) parser.add_argument("--port", type=int, default=8080, help="Viser HTTP port") CloudXRLauncher.add_launcher_arguments(parser) args = parser.parse_args(argv[1:]) server = viser.ViserServer(host=args.host, port=args.port) - server.scene.set_up_direction("+y") - server.scene.add_grid(name="/grid", width=2.0, height=2.0, cell_size=0.1) + ground = setup_scene(server) config = TeleopSessionConfig( app_name="LiveFullBodyExample", @@ -57,11 +56,8 @@ def main(argv: list[str]) -> int: print("[live] waiting for headset connection… (Ctrl+C to stop)") with TeleopSession(config) as session: - viz = FullBodyViz(server) - print( - f"[live] viser listening on {args.host}:{args.port} " - f"(http://localhost:{args.port})" - ) + viz = FullBodyViz(server, ground) + print(f"[live] viser running at http://localhost:{args.port}") try: while True: result = session.step() diff --git a/examples/mcap_record_replay/python/live_hand.py b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/live_hand.py similarity index 86% rename from examples/mcap_record_replay/python/live_hand.py rename to examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/live_hand.py index 7349dea622..f06463b2fd 100644 --- a/examples/mcap_record_replay/python/live_hand.py +++ b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/live_hand.py @@ -26,23 +26,22 @@ from isaacteleop.cloudxr import CloudXRLauncher from isaacteleop.teleop_session_manager import TeleopSession, TeleopSessionConfig -from common import HandViz, LEFT_COLOR, RIGHT_COLOR, build_hand_pipeline +from .common import HandViz, LEFT_COLOR, RIGHT_COLOR, build_hand_pipeline, setup_scene def main(argv: list[str]) -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( "--host", - default="0.0.0.0", - help="Viser HTTP bind address (default: 0.0.0.0, all interfaces; pass 127.0.0.1 to keep it local)", + default="127.0.0.1", + help="Viser HTTP bind address (default: 127.0.0.1; pass 0.0.0.0 to expose externally)", ) parser.add_argument("--port", type=int, default=8080, help="Viser HTTP port") CloudXRLauncher.add_launcher_arguments(parser) args = parser.parse_args(argv[1:]) server = viser.ViserServer(host=args.host, port=args.port) - server.scene.set_up_direction("+y") - server.scene.add_grid(name="/grid", width=2.0, height=2.0, cell_size=0.1) + setup_scene(server) config = TeleopSessionConfig( app_name="LiveHandExample", @@ -57,10 +56,7 @@ def main(argv: list[str]) -> int: with TeleopSession(config) as session: viz_left = HandViz(server, "hand_left", LEFT_COLOR) viz_right = HandViz(server, "hand_right", RIGHT_COLOR) - print( - f"[live] viser listening on {args.host}:{args.port} " - f"(http://localhost:{args.port})" - ) + print(f"[live] viser running at http://localhost:{args.port}") _last_step_t = time.time() _missed = 0 try: diff --git a/examples/mcap_record_replay/python/record_controller.py b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/record_controller.py similarity index 94% rename from examples/mcap_record_replay/python/record_controller.py rename to examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/record_controller.py index 87fb58bce3..9fef543089 100644 --- a/examples/mcap_record_replay/python/record_controller.py +++ b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/record_controller.py @@ -12,7 +12,7 @@ Usage: python record_controller.py [duration_seconds] [output.mcap] [--accept-eula] -Defaults: 5 seconds → ../recordings/controllers_.mcap +Defaults: 5 seconds → ./recordings/controllers_.mcap See: https://nvidia.github.io/IsaacTeleop/main/references/mcap_record_replay.html """ @@ -28,7 +28,7 @@ from isaacteleop.retargeting_engine.tensor_types.indices import ControllerInputIndex from isaacteleop.teleop_session_manager import TeleopSession, TeleopSessionConfig -from common import build_controller_pipeline +from .common import build_controller_pipeline def main(argv: list[str]) -> int: @@ -46,7 +46,7 @@ def main(argv: list[str]) -> int: mcap_path = Path(args.output) mcap_path.parent.mkdir(parents=True, exist_ok=True) else: - out_dir = Path(__file__).resolve().parent.parent / "recordings" + out_dir = Path.cwd() / "recordings" out_dir.mkdir(exist_ok=True) mcap_path = out_dir / f"controllers_{datetime.now():%Y%m%d_%H%M%S}.mcap" diff --git a/examples/mcap_record_replay/python/record_full_body.py b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/record_full_body.py similarity index 94% rename from examples/mcap_record_replay/python/record_full_body.py rename to examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/record_full_body.py index d2c76c3577..0c6a03bb92 100644 --- a/examples/mcap_record_replay/python/record_full_body.py +++ b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/record_full_body.py @@ -12,7 +12,7 @@ Usage: python record_full_body.py [duration_seconds] [output.mcap] [--accept-eula] -Defaults: 5 seconds → ../recordings/full_body_.mcap +Defaults: 5 seconds → ./recordings/full_body_.mcap See: https://nvidia.github.io/IsaacTeleop/main/references/mcap_record_replay.html """ @@ -30,7 +30,7 @@ from isaacteleop.retargeting_engine.tensor_types.indices import FullBodyInputIndex from isaacteleop.teleop_session_manager import TeleopSession, TeleopSessionConfig -from common import BODY_JOINT_NAMES, build_full_body_pipeline +from .common import BODY_JOINT_NAMES, build_full_body_pipeline def main(argv: list[str]) -> int: @@ -48,7 +48,7 @@ def main(argv: list[str]) -> int: mcap_path = Path(args.output) mcap_path.parent.mkdir(parents=True, exist_ok=True) else: - out_dir = Path(__file__).resolve().parent.parent / "recordings" + out_dir = Path.cwd() / "recordings" out_dir.mkdir(exist_ok=True) mcap_path = out_dir / f"full_body_{datetime.now():%Y%m%d_%H%M%S}.mcap" diff --git a/examples/mcap_record_replay/python/record_hand.py b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/record_hand.py similarity index 94% rename from examples/mcap_record_replay/python/record_hand.py rename to examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/record_hand.py index cefa06ee8a..af39998253 100644 --- a/examples/mcap_record_replay/python/record_hand.py +++ b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/record_hand.py @@ -12,7 +12,7 @@ Usage: python record_hand.py [duration_seconds] [output.mcap] [--accept-eula] -Defaults: 5 seconds → ../recordings/hands_.mcap +Defaults: 5 seconds → ./recordings/hands_.mcap See: https://nvidia.github.io/IsaacTeleop/main/references/mcap_record_replay.html """ @@ -27,7 +27,7 @@ from isaacteleop.deviceio import McapRecordingConfig from isaacteleop.teleop_session_manager import TeleopSession, TeleopSessionConfig -from common import build_hand_pipeline +from .common import build_hand_pipeline def main(argv: list[str]) -> int: @@ -45,7 +45,7 @@ def main(argv: list[str]) -> int: mcap_path = Path(args.output) mcap_path.parent.mkdir(parents=True, exist_ok=True) else: - out_dir = Path(__file__).resolve().parent.parent / "recordings" + out_dir = Path.cwd() / "recordings" out_dir.mkdir(exist_ok=True) mcap_path = out_dir / f"hands_{datetime.now():%Y%m%d_%H%M%S}.mcap" diff --git a/examples/mcap_record_replay/python/record_se3_vive.py b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/record_se3_vive.py similarity index 97% rename from examples/mcap_record_replay/python/record_se3_vive.py rename to examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/record_se3_vive.py index 3e19b47c7d..aba35c90e0 100644 --- a/examples/mcap_record_replay/python/record_se3_vive.py +++ b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/record_se3_vive.py @@ -15,7 +15,7 @@ (VIVEHub tracker_server running, or VIVE_SE3_SYNTHETIC=1 for a smoke test) With no arguments it records 10 s of every collection the pusher currently -advertises (see --collections), to a timestamped file under ../recordings/. +advertises (see --collections), to a timestamped file under ./recordings/. Usage: source ~/.cloudxr/run/cloudxr.env @@ -85,7 +85,7 @@ def main(argv: list[str]) -> int: mcap_path = Path(args.output) mcap_path.parent.mkdir(parents=True, exist_ok=True) else: - out_dir = Path(__file__).resolve().parent.parent / "recordings" + out_dir = Path.cwd() / "recordings" out_dir.mkdir(exist_ok=True) mcap_path = out_dir / f"se3_vive_{datetime.now():%Y%m%d_%H%M%S}.mcap" diff --git a/examples/mcap_record_replay/python/replay_controller.py b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_controller.py similarity index 90% rename from examples/mcap_record_replay/python/replay_controller.py rename to examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_controller.py index 426d9b7c3b..79d7c8fd54 100644 --- a/examples/mcap_record_replay/python/replay_controller.py +++ b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_controller.py @@ -12,7 +12,7 @@ Usage: python replay_controller.py [path/to/file.mcap] [--port 8080] [--loop] -If no path is given, the newest file under ``../recordings/`` is used. +If no path is given, the newest file under ``./recordings/`` is used. ``--loop`` keeps replaying the file end-to-end until the process is killed. See: https://nvidia.github.io/IsaacTeleop/main/references/mcap_record_replay.html @@ -33,7 +33,8 @@ TeleopSessionConfig, ) -from common import ( +from .common import ( + setup_scene, ControllerViz, LEFT_COLOR, RIGHT_COLOR, @@ -67,7 +68,7 @@ def resolve_mcap(path_arg: str | None) -> Path: sys.exit(f"[replay] error: {path} does not exist") return path - recordings = Path(__file__).resolve().parent.parent / "recordings" + recordings = Path.cwd() / "recordings" candidates = list(recordings.glob("*.mcap")) if not candidates: sys.exit( @@ -121,8 +122,8 @@ def main(argv: list[str]) -> int: parser.add_argument("mcap", nargs="?", help="Path to .mcap file") parser.add_argument( "--host", - default="0.0.0.0", - help="Viser HTTP bind address (default: 0.0.0.0, all interfaces; pass 127.0.0.1 to keep it local)", + default="127.0.0.1", + help="Viser HTTP bind address (default: 127.0.0.1; pass 0.0.0.0 to expose externally)", ) parser.add_argument("--port", type=int, default=8080, help="Viser HTTP port") parser.add_argument( @@ -136,16 +137,12 @@ def main(argv: list[str]) -> int: duration_s = mcap_duration_s(mcap_path) server = viser.ViserServer(host=args.host, port=args.port) - server.scene.set_up_direction("+y") - server.scene.add_grid(name="/grid", width=2.0, height=2.0, cell_size=0.1) + setup_scene(server) viz_left = ControllerViz(server, "controller_left", LEFT_COLOR) viz_right = ControllerViz(server, "controller_right", RIGHT_COLOR) - print( - f"[replay] viser listening on {args.host}:{args.port} " - f"(http://localhost:{args.port})" - ) + print(f"[replay] viser running at http://localhost:{args.port}") print(f"[replay] reading {mcap_path} (duration {duration_s:.2f}s)") while True: diff --git a/examples/mcap_record_replay/python/replay_full_body.py b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_full_body.py similarity index 88% rename from examples/mcap_record_replay/python/replay_full_body.py rename to examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_full_body.py index 336a2f4b03..ffa923b546 100644 --- a/examples/mcap_record_replay/python/replay_full_body.py +++ b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_full_body.py @@ -11,7 +11,7 @@ Usage: python replay_full_body.py [path/to/file.mcap] [--port 8080] [--loop] -If no path is given, the newest file under ``../recordings/`` is used. +If no path is given, the newest file under ``./recordings/`` is used. ``--loop`` keeps replaying the file end-to-end until the process is killed. See: https://nvidia.github.io/IsaacTeleop/main/references/mcap_record_replay.html @@ -34,7 +34,7 @@ TeleopSessionConfig, ) -from common import BODY_JOINT_NAMES, FullBodyViz, build_full_body_pipeline +from .common import BODY_JOINT_NAMES, FullBodyViz, build_full_body_pipeline, setup_scene def mcap_duration_s(path: Path) -> float: @@ -62,7 +62,7 @@ def resolve_mcap(path_arg: str | None) -> Path: sys.exit(f"[replay] error: {path} does not exist") return path - recordings = Path(__file__).resolve().parent.parent / "recordings" + recordings = Path.cwd() / "recordings" candidates = list(recordings.glob("*.mcap")) if not candidates: sys.exit( @@ -122,8 +122,8 @@ def main(argv: list[str]) -> int: parser.add_argument("mcap", nargs="?", help="Path to .mcap file") parser.add_argument( "--host", - default="0.0.0.0", - help="Viser HTTP bind address (default: 0.0.0.0, all interfaces; pass 127.0.0.1 to keep it local)", + default="127.0.0.1", + help="Viser HTTP bind address (default: 127.0.0.1; pass 0.0.0.0 to expose externally)", ) parser.add_argument("--port", type=int, default=8080, help="Viser HTTP port") parser.add_argument( @@ -137,14 +137,10 @@ def main(argv: list[str]) -> int: duration_s = mcap_duration_s(mcap_path) server = viser.ViserServer(host=args.host, port=args.port) - server.scene.set_up_direction("+y") - server.scene.add_grid(name="/grid", width=2.0, height=2.0, cell_size=0.1) - viz = FullBodyViz(server) + ground = setup_scene(server) + viz = FullBodyViz(server, ground) - print( - f"[replay] viser listening on {args.host}:{args.port} " - f"(http://localhost:{args.port})" - ) + print(f"[replay] viser running at http://localhost:{args.port}") print(f"[replay] reading {mcap_path} (duration {duration_s:.2f}s)") while True: diff --git a/examples/mcap_record_replay/python/replay_hand.py b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_hand.py similarity index 88% rename from examples/mcap_record_replay/python/replay_hand.py rename to examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_hand.py index 0402badc68..d790c85cc8 100644 --- a/examples/mcap_record_replay/python/replay_hand.py +++ b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_hand.py @@ -11,7 +11,7 @@ Usage: python replay_hand.py [path/to/file.mcap] [--port 8080] [--loop] -If no path is given, the newest file under ``../recordings/`` is used. +If no path is given, the newest file under ``./recordings/`` is used. ``--loop`` keeps replaying the file end-to-end until the process is killed. See: https://nvidia.github.io/IsaacTeleop/main/references/mcap_record_replay.html @@ -33,7 +33,7 @@ TeleopSessionConfig, ) -from common import HandViz, LEFT_COLOR, RIGHT_COLOR, build_hand_pipeline +from .common import HandViz, LEFT_COLOR, RIGHT_COLOR, build_hand_pipeline, setup_scene def mcap_duration_s(path: Path) -> float: @@ -61,7 +61,7 @@ def resolve_mcap(path_arg: str | None) -> Path: sys.exit(f"[replay] error: {path} does not exist") return path - recordings = Path(__file__).resolve().parent.parent / "recordings" + recordings = Path.cwd() / "recordings" candidates = list(recordings.glob("*.mcap")) if not candidates: sys.exit( @@ -117,8 +117,8 @@ def main(argv: list[str]) -> int: parser.add_argument("mcap", nargs="?", help="Path to .mcap file") parser.add_argument( "--host", - default="0.0.0.0", - help="Viser HTTP bind address (default: 0.0.0.0, all interfaces; pass 127.0.0.1 to keep it local)", + default="127.0.0.1", + help="Viser HTTP bind address (default: 127.0.0.1; pass 0.0.0.0 to expose externally)", ) parser.add_argument("--port", type=int, default=8080, help="Viser HTTP port") parser.add_argument( @@ -132,16 +132,12 @@ def main(argv: list[str]) -> int: duration_s = mcap_duration_s(mcap_path) server = viser.ViserServer(host=args.host, port=args.port) - server.scene.set_up_direction("+y") - server.scene.add_grid(name="/grid", width=2.0, height=2.0, cell_size=0.1) + setup_scene(server) viz_left = HandViz(server, "hand_left", LEFT_COLOR) viz_right = HandViz(server, "hand_right", RIGHT_COLOR) - print( - f"[replay] viser listening on {args.host}:{args.port} " - f"(http://localhost:{args.port})" - ) + print(f"[replay] viser running at http://localhost:{args.port}") print(f"[replay] reading {mcap_path} (duration {duration_s:.2f}s)") while True: diff --git a/examples/mcap_record_replay/python/replay_se3_vive.py b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_se3_vive.py similarity index 94% rename from examples/mcap_record_replay/python/replay_se3_vive.py rename to examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_se3_vive.py index 2b460addca..ccc096ffa0 100644 --- a/examples/mcap_record_replay/python/replay_se3_vive.py +++ b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_se3_vive.py @@ -9,7 +9,7 @@ tracker as a coordinate frame in a viser 3D view (browser). Replay needs no OpenXR runtime and no hardware. -With no arguments it replays the most recent recording under ../recordings/, +With no arguments it replays the most recent recording under ./recordings/, auto-discovers its collections, and plays back at the recording's own capture rate. Open the printed viser URL to see the trackers in 3D. @@ -39,10 +39,10 @@ def _summary(mcap_path: Path): def resolve_mcap(path_arg: str | None) -> Path: - """Use the given path, or the newest .mcap under ../recordings/.""" + """Use the given path, or the newest .mcap under ./recordings/.""" if path_arg: return Path(path_arg) - recordings = Path(__file__).resolve().parent.parent / "recordings" + recordings = Path.cwd() / "recordings" candidates = list(recordings.glob("se3_vive_*.mcap")) or list( recordings.glob("*.mcap") ) @@ -112,8 +112,11 @@ class Se3Viz: def __init__(self, server, collections: list[str]): import viser # noqa: F401 (import here so --no-viz runs without viser) - server.scene.set_up_direction("+y") - server.scene.add_grid(name="/grid", width=2.0, height=2.0, cell_size=0.1) + # Also deferred: common imports viser at module level, so a top-level + # import here would defeat --no-viz. + from .common import setup_scene + + setup_scene(server) self._frames = {} self._labels = {} for cid in collections: @@ -178,7 +181,7 @@ def main(argv: list[str]) -> int: parser.add_argument( "mcap", nargs="?", - help="Recording to replay (default: newest under ../recordings/)", + help="Recording to replay (default: newest under ./recordings/)", ) parser.add_argument( "--collections", @@ -199,8 +202,8 @@ def main(argv: list[str]) -> int: ) parser.add_argument( "--host", - default="0.0.0.0", - help="Viser HTTP bind address (default: 0.0.0.0, all interfaces; pass 127.0.0.1 to keep it local)", + default="127.0.0.1", + help="Viser HTTP bind address (default: 127.0.0.1; pass 0.0.0.0 to expose externally)", ) parser.add_argument("--port", type=int, default=8080, help="Viser HTTP port") args = parser.parse_args(argv[1:]) diff --git a/src/plugins/noitom_mocap/README.md b/src/plugins/noitom_mocap/README.md index 636ec5fdf2..f6d8cc750a 100644 --- a/src/plugins/noitom_mocap/README.md +++ b/src/plugins/noitom_mocap/README.md @@ -63,7 +63,7 @@ script: uv run python examples/noitom/record_noitom_full_body.py \ 10 examples/noitom/recordings/noitom_full_body.mcap -cd examples/mcap_record_replay/python -uv sync -uv run python replay_full_body.py ../../noitom/recordings/noitom_full_body.mcap +uv pip install -e ./examples/mcap_record_replay +python -m isaacteleop_examples.mcap_record_replay.replay_full_body \ + examples/noitom/recordings/noitom_full_body.mcap ``` diff --git a/src/plugins/vive_se3_tracker/README.md b/src/plugins/vive_se3_tracker/README.md index 4ff1b95f57..23ddfba7d3 100644 --- a/src/plugins/vive_se3_tracker/README.md +++ b/src/plugins/vive_se3_tracker/README.md @@ -102,16 +102,15 @@ with no arguments. Stale files from a crashed run are cleared on the next startu ## Recording / replay (Python examples) -Both run with no arguments once the plugin is up (from the example directory, so -`uv` picks up its `pyproject.toml`): +Both run with no arguments once the plugin is up: ```bash -cd examples/mcap_record_replay/python -# records 10 s of every advertised collection to ../recordings/.mcap -uv run record_se3_vive.py +uv pip install -e ./examples/mcap_record_replay +# records 10 s of every advertised collection to ./recordings/.mcap +python -m isaacteleop_examples.mcap_record_replay.record_se3_vive # replays the newest recording, auto-discovering collections + capture rate, # and serves a viser 3D view at http://localhost:8080 -uv run replay_se3_vive.py +python -m isaacteleop_examples.mcap_record_replay.replay_se3_vive ``` Under the hood this is just the standard MCAP tooling — a `core::Se3Tracker(cid)` From 5acc5b06cf2c0c86d70f04772fac27a8ebe2074d Mon Sep 17 00:00:00 2001 From: Jiwen Cai Date: Tue, 8 Sep 2026 17:34:52 +0000 Subject: [PATCH 2/8] examples(mcap_record_replay): bind viser viewers to every interface by default Split out per review on #1038: these run on a robot or workstation and get opened from a laptop, same rationale as #1034's viser default flip. 127.0.0.1 stays available via --host. Signed-off-by: Jiwen Cai --- .../mcap_record_replay/live_controller.py | 12 ++++++++---- .../mcap_record_replay/live_full_body.py | 12 ++++++++---- .../mcap_record_replay/live_hand.py | 12 ++++++++---- .../mcap_record_replay/replay_controller.py | 9 ++++++--- .../mcap_record_replay/replay_full_body.py | 9 ++++++--- .../mcap_record_replay/replay_hand.py | 9 ++++++--- .../mcap_record_replay/replay_se3_vive.py | 4 ++-- 7 files changed, 44 insertions(+), 23 deletions(-) diff --git a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/live_controller.py b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/live_controller.py index d1c7b998ee..ffaeb769eb 100644 --- a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/live_controller.py +++ b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/live_controller.py @@ -5,7 +5,8 @@ Visualize live OpenXR controller poses in real time with viser. ``CloudXRLauncher`` starts the CloudXR runtime and WSS proxy automatically. -Open the URL viser prints (default http://localhost:8080) in a browser to see +Open the URL viser prints in a browser (binds all interfaces, so another +machine can reach it at http://:8080) to see aim / grip points for both controllers, a ray between them, and a live HUD showing thumbstick, trigger, squeeze, and button state. @@ -40,8 +41,8 @@ def main(argv: list[str]) -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( "--host", - default="127.0.0.1", - help="Viser HTTP bind address (default: 127.0.0.1; pass 0.0.0.0 to expose externally)", + default="0.0.0.0", + help="Viser HTTP bind address (default: 0.0.0.0, all interfaces; pass 127.0.0.1 to keep it local)", ) parser.add_argument("--port", type=int, default=8080, help="Viser HTTP port") CloudXRLauncher.add_launcher_arguments(parser) @@ -63,7 +64,10 @@ def main(argv: list[str]) -> int: with TeleopSession(config) as session: viz_left = ControllerViz(server, "controller_left", LEFT_COLOR) viz_right = ControllerViz(server, "controller_right", RIGHT_COLOR) - print(f"[live] viser running at http://localhost:{args.port}") + print( + f"[live] viser listening on {args.host}:{args.port} " + f"(http://localhost:{args.port})" + ) _last_step_t = time.time() _missed = 0 try: diff --git a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/live_full_body.py b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/live_full_body.py index 702ca2fb82..fcd699146d 100644 --- a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/live_full_body.py +++ b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/live_full_body.py @@ -5,7 +5,8 @@ Visualize live OpenXR full-body pose tracking in real time with viser. ``CloudXRLauncher`` starts the CloudXR runtime and WSS proxy automatically. -Open the URL viser prints (default http://localhost:8080) in a browser to see +Open the URL viser prints in a browser (binds all interfaces, so another +machine can reach it at http://:8080) to see the full PICO body skeleton — joints colored green when valid, red when lost — updating live as you move. @@ -35,8 +36,8 @@ def main(argv: list[str]) -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( "--host", - default="127.0.0.1", - help="Viser HTTP bind address (default: 127.0.0.1; pass 0.0.0.0 to expose externally)", + default="0.0.0.0", + help="Viser HTTP bind address (default: 0.0.0.0, all interfaces; pass 127.0.0.1 to keep it local)", ) parser.add_argument("--port", type=int, default=8080, help="Viser HTTP port") CloudXRLauncher.add_launcher_arguments(parser) @@ -57,7 +58,10 @@ def main(argv: list[str]) -> int: with TeleopSession(config) as session: viz = FullBodyViz(server, ground) - print(f"[live] viser running at http://localhost:{args.port}") + print( + f"[live] viser listening on {args.host}:{args.port} " + f"(http://localhost:{args.port})" + ) try: while True: result = session.step() diff --git a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/live_hand.py b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/live_hand.py index f06463b2fd..cf946f5c2e 100644 --- a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/live_hand.py +++ b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/live_hand.py @@ -5,7 +5,8 @@ Visualize live OpenXR hand-tracking in real time with viser. ``CloudXRLauncher`` starts the CloudXR runtime and WSS proxy automatically. -Open the URL viser prints (default http://localhost:8080) in a browser to see +Open the URL viser prints in a browser (binds all interfaces, so another +machine can reach it at http://:8080) to see both hands rendered as joint clouds + bone segments, updating live as you move. Usage: @@ -33,8 +34,8 @@ def main(argv: list[str]) -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( "--host", - default="127.0.0.1", - help="Viser HTTP bind address (default: 127.0.0.1; pass 0.0.0.0 to expose externally)", + default="0.0.0.0", + help="Viser HTTP bind address (default: 0.0.0.0, all interfaces; pass 127.0.0.1 to keep it local)", ) parser.add_argument("--port", type=int, default=8080, help="Viser HTTP port") CloudXRLauncher.add_launcher_arguments(parser) @@ -56,7 +57,10 @@ def main(argv: list[str]) -> int: with TeleopSession(config) as session: viz_left = HandViz(server, "hand_left", LEFT_COLOR) viz_right = HandViz(server, "hand_right", RIGHT_COLOR) - print(f"[live] viser running at http://localhost:{args.port}") + print( + f"[live] viser listening on {args.host}:{args.port} " + f"(http://localhost:{args.port})" + ) _last_step_t = time.time() _missed = 0 try: diff --git a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_controller.py b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_controller.py index 79d7c8fd54..12e9d6d20f 100644 --- a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_controller.py +++ b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_controller.py @@ -122,8 +122,8 @@ def main(argv: list[str]) -> int: parser.add_argument("mcap", nargs="?", help="Path to .mcap file") parser.add_argument( "--host", - default="127.0.0.1", - help="Viser HTTP bind address (default: 127.0.0.1; pass 0.0.0.0 to expose externally)", + default="0.0.0.0", + help="Viser HTTP bind address (default: 0.0.0.0, all interfaces; pass 127.0.0.1 to keep it local)", ) parser.add_argument("--port", type=int, default=8080, help="Viser HTTP port") parser.add_argument( @@ -142,7 +142,10 @@ def main(argv: list[str]) -> int: viz_left = ControllerViz(server, "controller_left", LEFT_COLOR) viz_right = ControllerViz(server, "controller_right", RIGHT_COLOR) - print(f"[replay] viser running at http://localhost:{args.port}") + print( + f"[replay] viser listening on {args.host}:{args.port} " + f"(http://localhost:{args.port})" + ) print(f"[replay] reading {mcap_path} (duration {duration_s:.2f}s)") while True: diff --git a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_full_body.py b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_full_body.py index ffa923b546..1ddacb4e28 100644 --- a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_full_body.py +++ b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_full_body.py @@ -122,8 +122,8 @@ def main(argv: list[str]) -> int: parser.add_argument("mcap", nargs="?", help="Path to .mcap file") parser.add_argument( "--host", - default="127.0.0.1", - help="Viser HTTP bind address (default: 127.0.0.1; pass 0.0.0.0 to expose externally)", + default="0.0.0.0", + help="Viser HTTP bind address (default: 0.0.0.0, all interfaces; pass 127.0.0.1 to keep it local)", ) parser.add_argument("--port", type=int, default=8080, help="Viser HTTP port") parser.add_argument( @@ -140,7 +140,10 @@ def main(argv: list[str]) -> int: ground = setup_scene(server) viz = FullBodyViz(server, ground) - print(f"[replay] viser running at http://localhost:{args.port}") + print( + f"[replay] viser listening on {args.host}:{args.port} " + f"(http://localhost:{args.port})" + ) print(f"[replay] reading {mcap_path} (duration {duration_s:.2f}s)") while True: diff --git a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_hand.py b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_hand.py index d790c85cc8..e7f8c40f22 100644 --- a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_hand.py +++ b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_hand.py @@ -117,8 +117,8 @@ def main(argv: list[str]) -> int: parser.add_argument("mcap", nargs="?", help="Path to .mcap file") parser.add_argument( "--host", - default="127.0.0.1", - help="Viser HTTP bind address (default: 127.0.0.1; pass 0.0.0.0 to expose externally)", + default="0.0.0.0", + help="Viser HTTP bind address (default: 0.0.0.0, all interfaces; pass 127.0.0.1 to keep it local)", ) parser.add_argument("--port", type=int, default=8080, help="Viser HTTP port") parser.add_argument( @@ -137,7 +137,10 @@ def main(argv: list[str]) -> int: viz_left = HandViz(server, "hand_left", LEFT_COLOR) viz_right = HandViz(server, "hand_right", RIGHT_COLOR) - print(f"[replay] viser running at http://localhost:{args.port}") + print( + f"[replay] viser listening on {args.host}:{args.port} " + f"(http://localhost:{args.port})" + ) print(f"[replay] reading {mcap_path} (duration {duration_s:.2f}s)") while True: diff --git a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_se3_vive.py b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_se3_vive.py index ccc096ffa0..65a802fa10 100644 --- a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_se3_vive.py +++ b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_se3_vive.py @@ -202,8 +202,8 @@ def main(argv: list[str]) -> int: ) parser.add_argument( "--host", - default="127.0.0.1", - help="Viser HTTP bind address (default: 127.0.0.1; pass 0.0.0.0 to expose externally)", + default="0.0.0.0", + help="Viser HTTP bind address (default: 0.0.0.0, all interfaces; pass 127.0.0.1 to keep it local)", ) parser.add_argument("--port", type=int, default=8080, help="Viser HTTP port") args = parser.parse_args(argv[1:]) From 9fe25bd9809c89ac4b03587dd14acc07aee313cd Mon Sep 17 00:00:00 2001 From: Jiwen Cai Date: Tue, 8 Sep 2026 17:35:11 +0000 Subject: [PATCH 3/8] examples(mcap_record_replay): filter replay auto-discovery by recording type resolve_mcap() picked the newest .mcap in the shared ./recordings/ directory regardless of source. If a replay's own recorder hasn't produced a file yet, a newer recording from a different recorder can be selected and fail with a missing-channel error. Prefer a type-prefixed match, falling back to the newest .mcap of any kind -- matching replay_se3_vive.py's existing pattern. Flagged by CodeRabbit on #1038. Signed-off-by: Jiwen Cai --- .../mcap_record_replay/replay_controller.py | 4 +++- .../mcap_record_replay/replay_full_body.py | 4 +++- .../isaacteleop_examples/mcap_record_replay/replay_hand.py | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_controller.py b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_controller.py index 12e9d6d20f..92642185fb 100644 --- a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_controller.py +++ b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_controller.py @@ -69,7 +69,9 @@ def resolve_mcap(path_arg: str | None) -> Path: return path recordings = Path.cwd() / "recordings" - candidates = list(recordings.glob("*.mcap")) + candidates = list(recordings.glob("controllers_*.mcap")) or list( + recordings.glob("*.mcap") + ) if not candidates: sys.exit( f"[replay] error: no .mcap files in {recordings}. " diff --git a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_full_body.py b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_full_body.py index 1ddacb4e28..72e4ee7061 100644 --- a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_full_body.py +++ b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_full_body.py @@ -63,7 +63,9 @@ def resolve_mcap(path_arg: str | None) -> Path: return path recordings = Path.cwd() / "recordings" - candidates = list(recordings.glob("*.mcap")) + candidates = list(recordings.glob("full_body_*.mcap")) or list( + recordings.glob("*.mcap") + ) if not candidates: sys.exit( f"[replay] error: no .mcap files in {recordings}. " diff --git a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_hand.py b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_hand.py index e7f8c40f22..6afa3e86b3 100644 --- a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_hand.py +++ b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_hand.py @@ -62,7 +62,9 @@ def resolve_mcap(path_arg: str | None) -> Path: return path recordings = Path.cwd() / "recordings" - candidates = list(recordings.glob("*.mcap")) + candidates = list(recordings.glob("hands_*.mcap")) or list( + recordings.glob("*.mcap") + ) if not candidates: sys.exit( f"[replay] error: no .mcap files in {recordings}. " From ec124af948a8ae414ad5e373646b5db2cbd77845 Mon Sep 17 00:00:00 2001 From: Jiwen Cai Date: Tue, 8 Sep 2026 17:35:22 +0000 Subject: [PATCH 4/8] examples(mcap_record_replay): ignore /recordings/ written from the repo root The recorders default to Path.cwd() / "recordings", and the README has you run from the repo root, so the existing examples/mcap_record_replay/.gitignore rule (scoped to that directory) never covers the output. Add a root-level /recordings/ rule. Flagged in review on #1038. Signed-off-by: Jiwen Cai --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 45c2754576..3b4043a02c 100644 --- a/.gitignore +++ b/.gitignore @@ -59,5 +59,9 @@ 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. +/recordings/ + # Runtime device files injected by MCP server infrastructure .mcp.json From 454249403280a053971a438236b79c510b95515b Mon Sep 17 00:00:00 2001 From: Jiwen Cai Date: Sat, 12 Sep 2026 18:35:57 +0000 Subject: [PATCH 5/8] examples(mcap_record_replay): align user-visible strings with the -m invocation Usage lines, --help text (argparse reads it from the module docstring), and error messages still told users to run these as scripts (python replay_full_body.py ...) after the move to package/module layout. Switch them to python -m isaacteleop_examples.mcap_record_replay., matching the README and rst; fix the same in the rst's Replaying section, which was the one block left in script form. Also updates the 'newest file' wording (docstrings, README, rst) for the type-prefixed auto-discovery from the prior commit, warns instead of silently degrading when resolve_mcap falls back to a wrong-type recording, and documents the --python 3.11 requirement for uv run. Flagged by ivany-nv's verification pass on #1038. Signed-off-by: Jiwen Cai --- docs/source/references/mcap_record_replay.rst | 22 +++++++++------- examples/mcap_record_replay/README.md | 8 +++++- .../mcap_record_replay/live_controller.py | 2 +- .../mcap_record_replay/live_full_body.py | 2 +- .../mcap_record_replay/live_hand.py | 2 +- .../mcap_record_replay/record_controller.py | 2 +- .../mcap_record_replay/record_full_body.py | 2 +- .../mcap_record_replay/record_hand.py | 2 +- .../mcap_record_replay/record_se3_vive.py | 4 +-- .../mcap_record_replay/replay_controller.py | 25 ++++++++++++------- .../mcap_record_replay/replay_full_body.py | 25 ++++++++++++------- .../mcap_record_replay/replay_hand.py | 25 ++++++++++++------- .../mcap_record_replay/replay_se3_vive.py | 6 ++--- 13 files changed, 79 insertions(+), 48 deletions(-) diff --git a/docs/source/references/mcap_record_replay.rst b/docs/source/references/mcap_record_replay.rst index a9025911ba..600e996c10 100644 --- a/docs/source/references/mcap_record_replay.rst +++ b/docs/source/references/mcap_record_replay.rst @@ -222,19 +222,23 @@ Replay runs headless — no headset required: .. code-block:: bash - uv run python replay_hand.py # newest file in ../recordings/ - uv run python replay_hand.py path/to/file.mcap # explicit file - uv run python replay_hand.py --loop # repeat until Ctrl+C - uv run python replay_hand.py --port 8090 # change viser port + R="python -m isaacteleop_examples.mcap_record_replay.replay_hand" + $R # newest hands_*.mcap under ./recordings/ + $R path/to/file.mcap # explicit file + $R --loop # repeat until Ctrl+C + $R --port 8090 # change viser port Open the printed URL (default ``http://localhost:8080``) in a browser to see the left (green) and right (blue) hand skeletons update each frame. -The ``record_controller.py`` / ``replay_controller.py`` and -``record_full_body.py`` / ``replay_full_body.py`` pairs use the same CLI -(positional MCAP path, ``--host``, ``--port``, ``--loop``). -``live_full_body.py`` accepts ``--host``, ``--port``, and the CloudXR launcher -flags (including ``--accept-eula``). +The ``record_controller`` / ``replay_controller`` and ``record_full_body`` / +``replay_full_body`` module pairs use the same CLI (positional MCAP path, +``--host``, ``--port``, ``--loop``). ``live_full_body`` accepts ``--host``, +``--port``, and the CloudXR launcher flags (including ``--accept-eula``). + +``uv run`` (instead of an activated venv) needs an explicit ``--python 3.11`` +— without it, ``uv`` may resolve against the system Python and fail to match +the lockfile. API Reference ------------- diff --git a/examples/mcap_record_replay/README.md b/examples/mcap_record_replay/README.md index f77976d153..49985ee644 100644 --- a/examples/mcap_record_replay/README.md +++ b/examples/mcap_record_replay/README.md @@ -15,7 +15,13 @@ python -m isaacteleop_examples.mcap_record_replay.replay_hand # newest take ``` Recordings are written to `./recordings/` relative to where you run the -command; a replay given no path picks the newest file there. +command; a replay given no path picks the newest matching recording there +(e.g. `replay_hand` picks the newest `hands_*.mcap`), falling back to the +newest `.mcap` of any kind if none match. + +`uv run` (instead of an activated venv) needs an explicit `--python 3.11` -- +without it, `uv` may resolve against the system Python and fail to match the +lockfile. The live and replay viewers bind every interface, so a browser on another machine can reach them at `http://:8080`. Pass `--host 127.0.0.1` to diff --git a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/live_controller.py b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/live_controller.py index ffaeb769eb..a13e91535d 100644 --- a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/live_controller.py +++ b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/live_controller.py @@ -11,7 +11,7 @@ showing thumbstick, trigger, squeeze, and button state. Usage: - python live_controller.py [--port 8080] [--host 127.0.0.1] [--accept-eula] + python -m isaacteleop_examples.mcap_record_replay.live_controller [--port 8080] [--host 127.0.0.1] [--accept-eula] Press Ctrl+C to stop. diff --git a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/live_full_body.py b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/live_full_body.py index fcd699146d..4de6efa71f 100644 --- a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/live_full_body.py +++ b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/live_full_body.py @@ -11,7 +11,7 @@ updating live as you move. Usage: - python live_full_body.py [--port 8080] [--host 127.0.0.1] [--accept-eula] + python -m isaacteleop_examples.mcap_record_replay.live_full_body [--port 8080] [--host 127.0.0.1] [--accept-eula] Press Ctrl+C to stop. diff --git a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/live_hand.py b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/live_hand.py index cf946f5c2e..8ad82ad5de 100644 --- a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/live_hand.py +++ b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/live_hand.py @@ -10,7 +10,7 @@ both hands rendered as joint clouds + bone segments, updating live as you move. Usage: - python live_hand.py [--port 8080] [--host 127.0.0.1] [--accept-eula] + python -m isaacteleop_examples.mcap_record_replay.live_hand [--port 8080] [--host 127.0.0.1] [--accept-eula] Press Ctrl+C to stop. diff --git a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/record_controller.py b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/record_controller.py index 9fef543089..2d074941c3 100644 --- a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/record_controller.py +++ b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/record_controller.py @@ -10,7 +10,7 @@ exactly the ``controllers`` channel — no head, no hands. Usage: - python record_controller.py [duration_seconds] [output.mcap] [--accept-eula] + python -m isaacteleop_examples.mcap_record_replay.record_controller [duration_seconds] [output.mcap] [--accept-eula] Defaults: 5 seconds → ./recordings/controllers_.mcap diff --git a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/record_full_body.py b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/record_full_body.py index 0c6a03bb92..c616c3ace1 100644 --- a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/record_full_body.py +++ b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/record_full_body.py @@ -10,7 +10,7 @@ ``TeleopSession`` records the ``full_body`` and ``controllers`` channels. Usage: - python record_full_body.py [duration_seconds] [output.mcap] [--accept-eula] + python -m isaacteleop_examples.mcap_record_replay.record_full_body [duration_seconds] [output.mcap] [--accept-eula] Defaults: 5 seconds → ./recordings/full_body_.mcap diff --git a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/record_hand.py b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/record_hand.py index af39998253..b8c9b644fa 100644 --- a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/record_hand.py +++ b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/record_hand.py @@ -10,7 +10,7 @@ the ``hands`` channel — no head, no controllers. Usage: - python record_hand.py [duration_seconds] [output.mcap] [--accept-eula] + python -m isaacteleop_examples.mcap_record_replay.record_hand [duration_seconds] [output.mcap] [--accept-eula] Defaults: 5 seconds → ./recordings/hands_.mcap diff --git a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/record_se3_vive.py b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/record_se3_vive.py index aba35c90e0..c46ab11326 100644 --- a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/record_se3_vive.py +++ b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/record_se3_vive.py @@ -7,7 +7,7 @@ Reads the per-tracker "vive_tracker_" tensor collections pushed by the vive_se3_tracker plugin (one core.Se3Tracker per collection) and records each one to its own MCAP channel pair ( / _tracked) via the standard -Se3TrackerRecordingTraits. Replay with replay_se3_vive.py. +Se3TrackerRecordingTraits. Replay with replay_se3_vive. Prerequisites (separate terminals): 1. CloudXR runtime: python -m isaacteleop.cloudxr @@ -19,7 +19,7 @@ Usage: source ~/.cloudxr/run/cloudxr.env - uv run record_se3_vive.py [duration_s] [output.mcap] [--collections a,b,c] + python -m isaacteleop_examples.mcap_record_replay.record_se3_vive [duration_s] [output.mcap] [--collections a,b,c] """ import argparse diff --git a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_controller.py b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_controller.py index 92642185fb..a969766032 100644 --- a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_controller.py +++ b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_controller.py @@ -10,9 +10,10 @@ HUD (thumbstick, trigger, squeeze, buttons). Usage: - python replay_controller.py [path/to/file.mcap] [--port 8080] [--loop] + python -m isaacteleop_examples.mcap_record_replay.replay_controller [path/to/file.mcap] [--port 8080] [--loop] -If no path is given, the newest file under ``./recordings/`` is used. +If no path is given, the newest ``controllers_*.mcap`` under ``./recordings/`` is +used, falling back to the newest ``.mcap`` of any kind if none match. ``--loop`` keeps replaying the file end-to-end until the process is killed. See: https://nvidia.github.io/IsaacTeleop/main/references/mcap_record_replay.html @@ -69,14 +70,20 @@ def resolve_mcap(path_arg: str | None) -> Path: return path recordings = Path.cwd() / "recordings" - candidates = list(recordings.glob("controllers_*.mcap")) or list( - recordings.glob("*.mcap") - ) + candidates = list(recordings.glob("controllers_*.mcap")) if not candidates: - sys.exit( - f"[replay] error: no .mcap files in {recordings}. " - "Run record_controller.py first or pass a path." - ) + candidates = list(recordings.glob("*.mcap")) + if candidates: + print( + f"[replay] warning: no controllers_*.mcap in {recordings}, " + "falling back to the newest .mcap of any kind -- it may not " + "match this replay's channels." + ) + else: + sys.exit( + f"[replay] error: no .mcap files in {recordings}. " + "Run record_controller first or pass a path." + ) return max(candidates, key=lambda p: p.stat().st_mtime) diff --git a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_full_body.py b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_full_body.py index 72e4ee7061..5e571b3369 100644 --- a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_full_body.py +++ b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_full_body.py @@ -9,9 +9,10 @@ http://localhost:8080) in a browser to see the body skeleton. Usage: - python replay_full_body.py [path/to/file.mcap] [--port 8080] [--loop] + python -m isaacteleop_examples.mcap_record_replay.replay_full_body [path/to/file.mcap] [--port 8080] [--loop] -If no path is given, the newest file under ``./recordings/`` is used. +If no path is given, the newest ``full_body_*.mcap`` under ``./recordings/`` is +used, falling back to the newest ``.mcap`` of any kind if none match. ``--loop`` keeps replaying the file end-to-end until the process is killed. See: https://nvidia.github.io/IsaacTeleop/main/references/mcap_record_replay.html @@ -63,14 +64,20 @@ def resolve_mcap(path_arg: str | None) -> Path: return path recordings = Path.cwd() / "recordings" - candidates = list(recordings.glob("full_body_*.mcap")) or list( - recordings.glob("*.mcap") - ) + candidates = list(recordings.glob("full_body_*.mcap")) if not candidates: - sys.exit( - f"[replay] error: no .mcap files in {recordings}. " - "Run record_full_body.py first or pass a path." - ) + candidates = list(recordings.glob("*.mcap")) + if candidates: + print( + f"[replay] warning: no full_body_*.mcap in {recordings}, " + "falling back to the newest .mcap of any kind -- it may not " + "match this replay's channels." + ) + else: + sys.exit( + f"[replay] error: no .mcap files in {recordings}. " + "Run record_full_body first or pass a path." + ) return max(candidates, key=lambda p: p.stat().st_mtime) diff --git a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_hand.py b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_hand.py index 6afa3e86b3..33e6c77e23 100644 --- a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_hand.py +++ b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_hand.py @@ -9,9 +9,10 @@ http://localhost:8080) in a browser to see the hands move. Usage: - python replay_hand.py [path/to/file.mcap] [--port 8080] [--loop] + python -m isaacteleop_examples.mcap_record_replay.replay_hand [path/to/file.mcap] [--port 8080] [--loop] -If no path is given, the newest file under ``./recordings/`` is used. +If no path is given, the newest ``hands_*.mcap`` under ``./recordings/`` is +used, falling back to the newest ``.mcap`` of any kind if none match. ``--loop`` keeps replaying the file end-to-end until the process is killed. See: https://nvidia.github.io/IsaacTeleop/main/references/mcap_record_replay.html @@ -62,14 +63,20 @@ def resolve_mcap(path_arg: str | None) -> Path: return path recordings = Path.cwd() / "recordings" - candidates = list(recordings.glob("hands_*.mcap")) or list( - recordings.glob("*.mcap") - ) + candidates = list(recordings.glob("hands_*.mcap")) if not candidates: - sys.exit( - f"[replay] error: no .mcap files in {recordings}. " - "Run record_hand.py first or pass a path." - ) + candidates = list(recordings.glob("*.mcap")) + if candidates: + print( + f"[replay] warning: no hands_*.mcap in {recordings}, " + "falling back to the newest .mcap of any kind -- it may not " + "match this replay's channels." + ) + else: + sys.exit( + f"[replay] error: no .mcap files in {recordings}. " + "Run record_hand first or pass a path." + ) return max(candidates, key=lambda p: p.stat().st_mtime) diff --git a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_se3_vive.py b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_se3_vive.py index 65a802fa10..5768af23a3 100644 --- a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_se3_vive.py +++ b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_se3_vive.py @@ -4,7 +4,7 @@ """ Replay VIVE Ultimate Tracker SE3 pose streams from an MCAP file — no runtime needed. -Opens a recording made by record_se3_vive.py, replays each per-tracker channel +Opens a recording made by record_se3_vive, replays each per-tracker channel through ReplaySession + core.Se3Tracker (ReplaySe3TrackerImpl), and shows every tracker as a coordinate frame in a viser 3D view (browser). Replay needs no OpenXR runtime and no hardware. @@ -14,7 +14,7 @@ rate. Open the printed viser URL to see the trackers in 3D. Usage: - uv run replay_se3_vive.py [recording.mcap] [--loop] [--rate N] [--no-viz] \ + python -m isaacteleop_examples.mcap_record_replay.replay_se3_vive [recording.mcap] [--loop] [--rate N] [--no-viz] \ [--collections a,b,c] """ @@ -48,7 +48,7 @@ def resolve_mcap(path_arg: str | None) -> Path: ) if not candidates: sys.exit( - f"[replay-se3] no .mcap files in {recordings}. Run record_se3_vive.py first." + f"[replay-se3] no .mcap files in {recordings}. Run record_se3_vive first." ) return max(candidates, key=lambda p: p.stat().st_mtime) From f28121241ad20dd1a2c7a417996b57e15126460a Mon Sep 17 00:00:00 2001 From: Jiwen Cai Date: Sat, 12 Sep 2026 18:36:04 +0000 Subject: [PATCH 6/8] examples: document why GroundGrid is duplicated across examples common.py's GroundGrid (mcap_record_replay) and deviceio_viser.py's (deviceio_live_view, landed on main via #1034) are byte-identical. Not deduping: each example package is self-contained by design (see examples/README.md), and a shared helper would make one example depend on another. Leave a pointer in each copy instead of letting the duplication look accidental. Flagged by ivany-nv's verification pass on #1038. Signed-off-by: Jiwen Cai --- .../isaacteleop_examples/deviceio_live_view/deviceio_viser.py | 3 +++ .../python/isaacteleop_examples/mcap_record_replay/common.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/examples/deviceio_live_view/python/isaacteleop_examples/deviceio_live_view/deviceio_viser.py b/examples/deviceio_live_view/python/isaacteleop_examples/deviceio_live_view/deviceio_viser.py index 5feac7f94f..059d32951a 100644 --- a/examples/deviceio_live_view/python/isaacteleop_examples/deviceio_live_view/deviceio_viser.py +++ b/examples/deviceio_live_view/python/isaacteleop_examples/deviceio_live_view/deviceio_viser.py @@ -42,6 +42,9 @@ TRACKED_COLOR: tuple[float, float, float] = (0.25, 0.85, 0.35) +# Duplicated verbatim in mcap_record_replay/common.py: each example package +# is self-contained (see examples/README.md), so this stays a copy rather +# than a cross-example dependency. Keep the two in sync by hand. class GroundGrid: """The ground plane and the default camera, anchored to what is tracked. diff --git a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/common.py b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/common.py index 76bf5340bb..f4aea8d3c9 100644 --- a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/common.py +++ b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/common.py @@ -48,6 +48,9 @@ ) +# Duplicated verbatim in deviceio_live_view/deviceio_viser.py: each example +# package is self-contained (see examples/README.md), so this stays a copy +# rather than a cross-example dependency. Keep the two in sync by hand. class GroundGrid: """The ground plane and the default camera, anchored to what is tracked. From 86861130ab17835018dad09c721e858d8dbbdfb0 Mon Sep 17 00:00:00 2001 From: Jiwen Cai Date: Mon, 14 Sep 2026 20:43:13 +0000 Subject: [PATCH 7/8] docs(mcap_record_replay): fix repo-root vs example-directory mismatch 'Live preview' and 'Recording' both said 'From the example directory:' but gave uv pip install -e ./examples/mcap_record_replay, which is repo-root-relative -- run from the example directory it resolves to a nonexistent nested path. Say what the commands actually assume; it also matches 887db467's rationale for the root .gitignore rule. Also drops a stale .py suffix on a replay_full_body module reference a few lines above. Flagged by ivany-nv on #1038. Signed-off-by: Jiwen Cai --- docs/source/references/mcap_record_replay.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/references/mcap_record_replay.rst b/docs/source/references/mcap_record_replay.rst index 600e996c10..5fd51939fc 100644 --- a/docs/source/references/mcap_record_replay.rst +++ b/docs/source/references/mcap_record_replay.rst @@ -168,12 +168,12 @@ A C++ recorder lives at ``examples/mcap_record_replay/cpp/``: - ``record_full_body.cpp`` — records the ``full_body`` channel by passing a ``core::McapRecordingConfig`` to ``DeviceIOSession::run()``. It uses the same channel base name as ``FullBodySource``, so the resulting file replays - with ``replay_full_body.py``. + with ``replay_full_body``. Live preview ^^^^^^^^^^^^ -From the example directory: +From the repo root: .. code-block:: bash @@ -188,7 +188,7 @@ opened from another machine; pass ``--host 127.0.0.1`` to keep one local. Recording ^^^^^^^^^ -From the example directory: +From the repo root: .. code-block:: bash From 857aa2db2cff71419b6bfb9cc3c0b119cdbd2dc4 Mon Sep 17 00:00:00 2001 From: Jiwen Cai Date: Fri, 18 Sep 2026 15:18:23 +0000 Subject: [PATCH 8/8] examples(mcap_record_replay): drop the wrong-type fallback in resolve_mcap The fallback to the newest .mcap of any kind (added to fix picking the wrong type outright) traded a hard failure for a silent one: a missed log line still means the wrong recording plays. Fail instead -- point at the recorder or an explicit path. Flagged by aristarkhovNV on #1038. Signed-off-by: Jiwen Cai --- examples/mcap_record_replay/README.md | 4 ++-- .../mcap_record_replay/replay_controller.py | 18 +++++------------- .../mcap_record_replay/replay_full_body.py | 18 +++++------------- .../mcap_record_replay/replay_hand.py | 18 +++++------------- 4 files changed, 17 insertions(+), 41 deletions(-) diff --git a/examples/mcap_record_replay/README.md b/examples/mcap_record_replay/README.md index 49985ee644..4ebcf7e88f 100644 --- a/examples/mcap_record_replay/README.md +++ b/examples/mcap_record_replay/README.md @@ -16,8 +16,8 @@ python -m isaacteleop_examples.mcap_record_replay.replay_hand # newest take Recordings are written to `./recordings/` relative to where you run the command; a replay given no path picks the newest matching recording there -(e.g. `replay_hand` picks the newest `hands_*.mcap`), falling back to the -newest `.mcap` of any kind if none match. +(e.g. `replay_hand` picks the newest `hands_*.mcap`) and exits if none +match -- no silent fallback to a wrong-type recording. `uv run` (instead of an activated venv) needs an explicit `--python 3.11` -- without it, `uv` may resolve against the system Python and fail to match the diff --git a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_controller.py b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_controller.py index a969766032..c49019aa3e 100644 --- a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_controller.py +++ b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_controller.py @@ -13,7 +13,7 @@ python -m isaacteleop_examples.mcap_record_replay.replay_controller [path/to/file.mcap] [--port 8080] [--loop] If no path is given, the newest ``controllers_*.mcap`` under ``./recordings/`` is -used, falling back to the newest ``.mcap`` of any kind if none match. +used. ``--loop`` keeps replaying the file end-to-end until the process is killed. See: https://nvidia.github.io/IsaacTeleop/main/references/mcap_record_replay.html @@ -72,18 +72,10 @@ def resolve_mcap(path_arg: str | None) -> Path: recordings = Path.cwd() / "recordings" candidates = list(recordings.glob("controllers_*.mcap")) if not candidates: - candidates = list(recordings.glob("*.mcap")) - if candidates: - print( - f"[replay] warning: no controllers_*.mcap in {recordings}, " - "falling back to the newest .mcap of any kind -- it may not " - "match this replay's channels." - ) - else: - sys.exit( - f"[replay] error: no .mcap files in {recordings}. " - "Run record_controller first or pass a path." - ) + sys.exit( + f"[replay] error: no controllers_*.mcap in {recordings}. " + "Run record_controller first or pass a path." + ) return max(candidates, key=lambda p: p.stat().st_mtime) diff --git a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_full_body.py b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_full_body.py index 5e571b3369..70a2fb3cb6 100644 --- a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_full_body.py +++ b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_full_body.py @@ -12,7 +12,7 @@ python -m isaacteleop_examples.mcap_record_replay.replay_full_body [path/to/file.mcap] [--port 8080] [--loop] If no path is given, the newest ``full_body_*.mcap`` under ``./recordings/`` is -used, falling back to the newest ``.mcap`` of any kind if none match. +used. ``--loop`` keeps replaying the file end-to-end until the process is killed. See: https://nvidia.github.io/IsaacTeleop/main/references/mcap_record_replay.html @@ -66,18 +66,10 @@ def resolve_mcap(path_arg: str | None) -> Path: recordings = Path.cwd() / "recordings" candidates = list(recordings.glob("full_body_*.mcap")) if not candidates: - candidates = list(recordings.glob("*.mcap")) - if candidates: - print( - f"[replay] warning: no full_body_*.mcap in {recordings}, " - "falling back to the newest .mcap of any kind -- it may not " - "match this replay's channels." - ) - else: - sys.exit( - f"[replay] error: no .mcap files in {recordings}. " - "Run record_full_body first or pass a path." - ) + sys.exit( + f"[replay] error: no full_body_*.mcap in {recordings}. " + "Run record_full_body first or pass a path." + ) return max(candidates, key=lambda p: p.stat().st_mtime) diff --git a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_hand.py b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_hand.py index 33e6c77e23..801b566aa3 100644 --- a/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_hand.py +++ b/examples/mcap_record_replay/python/isaacteleop_examples/mcap_record_replay/replay_hand.py @@ -12,7 +12,7 @@ python -m isaacteleop_examples.mcap_record_replay.replay_hand [path/to/file.mcap] [--port 8080] [--loop] If no path is given, the newest ``hands_*.mcap`` under ``./recordings/`` is -used, falling back to the newest ``.mcap`` of any kind if none match. +used. ``--loop`` keeps replaying the file end-to-end until the process is killed. See: https://nvidia.github.io/IsaacTeleop/main/references/mcap_record_replay.html @@ -65,18 +65,10 @@ def resolve_mcap(path_arg: str | None) -> Path: recordings = Path.cwd() / "recordings" candidates = list(recordings.glob("hands_*.mcap")) if not candidates: - candidates = list(recordings.glob("*.mcap")) - if candidates: - print( - f"[replay] warning: no hands_*.mcap in {recordings}, " - "falling back to the newest .mcap of any kind -- it may not " - "match this replay's channels." - ) - else: - sys.exit( - f"[replay] error: no .mcap files in {recordings}. " - "Run record_hand first or pass a path." - ) + sys.exit( + f"[replay] error: no hands_*.mcap in {recordings}. " + "Run record_hand first or pass a path." + ) return max(candidates, key=lambda p: p.stat().st_mtime)