From c68d5666a88e3432446e1f0444983b5ee3865f77 Mon Sep 17 00:00:00 2001 From: Jakob Bornecrantz Date: Wed, 24 Jun 2026 15:39:49 +0100 Subject: [PATCH 1/2] Add CloudXRLauncher CLI helpers for optional in-process runtime launch. Expose argparse helpers so embedding apps and examples can default to launching the CloudXR runtime and WSS proxy, with --no-launch-cloudxr-runtime for environments where the runtime is already running. Signed-off-by: Jakob Bornecrantz --- src/core/cloudxr/python/launcher.py | 108 +++++++++++++++++- .../cloudxr_tests/python/test_launcher.py | 74 +++++++++++- 2 files changed, 177 insertions(+), 5 deletions(-) diff --git a/src/core/cloudxr/python/launcher.py b/src/core/cloudxr/python/launcher.py index bf9e48ba0..7065428aa 100644 --- a/src/core/cloudxr/python/launcher.py +++ b/src/core/cloudxr/python/launcher.py @@ -10,8 +10,10 @@ from __future__ import annotations +import argparse import asyncio import atexit +import contextlib import logging import os import signal @@ -166,6 +168,83 @@ def __init__( self._start_wss_proxy(wss_log_path) logger.info("CloudXR WSS proxy started (log=%s)", wss_log_path) + # ------------------------------------------------------------------ + # CLI helpers for embedding applications and examples + # ------------------------------------------------------------------ + + @staticmethod + def add_cloudxr_install_dir_argument(parser: argparse.ArgumentParser) -> None: + """Register ``--cloudxr-install-dir`` on ``parser`` (default ``~/.cloudxr``).""" + parser.add_argument( + "--cloudxr-install-dir", + type=str, + default=os.path.expanduser("~/.cloudxr"), + metavar="PATH", + help="CloudXR install directory (default: ~/.cloudxr)", + ) + + @staticmethod + def add_launch_cloudxr_runtime_argument(parser: argparse.ArgumentParser) -> None: + """Register ``--launch-cloudxr-runtime`` on ``parser``. + + Uses :class:`argparse.BooleanOptionalAction`, so callers may pass + ``--no-launch-cloudxr-runtime`` when the runtime is already running + (for example after sourcing ``~/.cloudxr/run/cloudxr.env``). + """ + parser.add_argument( + "--launch-cloudxr-runtime", + action=argparse.BooleanOptionalAction, + default=True, + help=( + "Launch the CloudXR runtime and WSS proxy in-process before running " + "(default: true). Pass --no-launch-cloudxr-runtime when the runtime is " + "already running (e.g. after sourcing ~/.cloudxr/run/cloudxr.env)." + ), + ) + + @staticmethod + def add_launcher_arguments(parser: argparse.ArgumentParser) -> None: + """Register ``--cloudxr-install-dir`` and ``--launch-cloudxr-runtime``.""" + CloudXRLauncher.add_cloudxr_install_dir_argument(parser) + CloudXRLauncher.add_launch_cloudxr_runtime_argument(parser) + + @staticmethod + def _resolve_install_dir( + args: argparse.Namespace, + install_dir: str | None = None, + ) -> str: + """Return ``install_dir`` or ``args.cloudxr_install_dir`` when registered.""" + if install_dir is not None: + return install_dir + return getattr(args, "cloudxr_install_dir", "~/.cloudxr") + + @staticmethod + def launch_context( + args: argparse.Namespace, + *, + install_dir: str | None = None, + env_config: str | Path | None = None, + accept_eula: bool = False, + setup_oob: bool = False, + usb_local: bool = False, + host_client: bool = False, + ) -> contextlib.AbstractContextManager[CloudXRLauncher | None]: + """Start :class:`CloudXRLauncher` when ``args.launch_cloudxr_runtime`` is true. + + Returns :func:`contextlib.nullcontext` when ``args.launch_cloudxr_runtime`` is + false so callers can always use ``with CloudXRLauncher.launch_context(args):``. + """ + if not args.launch_cloudxr_runtime: + return contextlib.nullcontext(None) + return CloudXRLauncher( + install_dir=CloudXRLauncher._resolve_install_dir(args, install_dir), + env_config=env_config, + accept_eula=accept_eula, + setup_oob=setup_oob, + usb_local=usb_local, + host_client=host_client, + ) + # ------------------------------------------------------------------ # Context manager # ------------------------------------------------------------------ @@ -339,15 +418,18 @@ def _is_runtime_alive(self) -> bool: def _terminate_runtime(self) -> None: """Terminate the runtime subprocess and all its children. - Because the subprocess is launched with ``start_new_session=True`` - it is the leader of its own process group. Sending the signal to - the negative PID kills the entire group (including Monado and any - other children), preventing stale processes from lingering. + On POSIX, the subprocess is launched with ``start_new_session=True`` + so it leads its own process group; ``killpg`` tears down Monado and + other children. On Windows, terminate the direct child only. """ proc = self._runtime_proc if proc is None or proc.poll() is not None: return + if sys.platform == "win32": + self._terminate_runtime_windows(proc) + return + try: pgid = os.getpgid(proc.pid) except ProcessLookupError: @@ -375,6 +457,24 @@ def _terminate_runtime(self) -> None: if proc.poll() is None: raise RuntimeError("Failed to terminate or kill runtime process group") + def _terminate_runtime_windows(self, proc: subprocess.Popen) -> None: + """Terminate the runtime subprocess on Windows (no process groups).""" + proc.terminate() + try: + proc.wait(timeout=RUNTIME_TERMINATE_TIMEOUT_SEC) + except subprocess.TimeoutExpired: + pass + + if proc.poll() is None: + proc.kill() + try: + proc.wait(timeout=RUNTIME_TERMINATE_TIMEOUT_SEC) + except subprocess.TimeoutExpired: + pass + + if proc.poll() is None: + raise RuntimeError("Failed to terminate or kill runtime process") + # ------------------------------------------------------------------ # WSS proxy (background thread with its own event loop) # ------------------------------------------------------------------ diff --git a/src/core/cloudxr_tests/python/test_launcher.py b/src/core/cloudxr_tests/python/test_launcher.py index c7ffbcbee..116a474ad 100644 --- a/src/core/cloudxr_tests/python/test_launcher.py +++ b/src/core/cloudxr_tests/python/test_launcher.py @@ -3,6 +3,7 @@ """Tests for isaacteleop.cloudxr.launcher — CloudXRLauncher lifecycle.""" +import argparse import os import signal import subprocess @@ -47,8 +48,11 @@ def ensure_logs_dir(self) -> Path: def _make_mock_popen(pid: int = 12345, poll_returns: list | None = None) -> MagicMock: """Create a mock subprocess.Popen with configurable poll() behaviour.""" - proc = MagicMock(spec=subprocess.Popen) + proc = MagicMock() proc.pid = pid + proc.terminate = MagicMock() + proc.kill = MagicMock() + proc.wait = MagicMock() if poll_returns is not None: seq = list(poll_returns) @@ -314,5 +318,73 @@ def test_handles_missing_fuser(self, tmp_path): assert not os.path.exists(cloudxr_pid) +class TestLaunchArgumentHelpers: + """Tests for CloudXRLauncher CLI helper methods.""" + + def test_add_cloudxr_install_dir_argument_default(self) -> None: + parser = argparse.ArgumentParser() + CloudXRLauncher.add_cloudxr_install_dir_argument(parser) + args = parser.parse_args([]) + assert args.cloudxr_install_dir == os.path.expanduser("~/.cloudxr") + + def test_add_cloudxr_install_dir_argument_custom(self) -> None: + parser = argparse.ArgumentParser() + CloudXRLauncher.add_cloudxr_install_dir_argument(parser) + args = parser.parse_args(["--cloudxr-install-dir", "/opt/cloudxr"]) + assert args.cloudxr_install_dir == "/opt/cloudxr" + + def test_add_launcher_arguments_registers_both(self) -> None: + parser = argparse.ArgumentParser() + CloudXRLauncher.add_launcher_arguments(parser) + args = parser.parse_args( + ["--cloudxr-install-dir", "/opt/cloudxr", "--no-launch-cloudxr-runtime"] + ) + assert args.cloudxr_install_dir == "/opt/cloudxr" + assert args.launch_cloudxr_runtime is False + + def test_add_launch_cloudxr_runtime_argument_defaults_true(self) -> None: + parser = argparse.ArgumentParser() + CloudXRLauncher.add_launch_cloudxr_runtime_argument(parser) + args = parser.parse_args([]) + assert args.launch_cloudxr_runtime is True + + def test_add_launch_cloudxr_runtime_argument_no_launch(self) -> None: + parser = argparse.ArgumentParser() + CloudXRLauncher.add_launch_cloudxr_runtime_argument(parser) + args = parser.parse_args(["--no-launch-cloudxr-runtime"]) + assert args.launch_cloudxr_runtime is False + + def test_launch_context_skips_when_disabled(self) -> None: + args = argparse.Namespace(launch_cloudxr_runtime=False) + with CloudXRLauncher.launch_context(args) as launcher: + assert launcher is None + + def test_launch_context_starts_when_enabled(self, tmp_path) -> None: + args = argparse.Namespace( + launch_cloudxr_runtime=True, + cloudxr_install_dir="/opt/cloudxr", + ) + with mock_launcher_deps(tmp_path) as mocks: + with CloudXRLauncher.launch_context(args) as launcher: + assert launcher is not None + assert launcher._runtime_proc is mocks["proc"] + assert launcher._install_dir == "/opt/cloudxr" + + def test_stop_on_windows_without_getpgid(self, tmp_path) -> None: + """stop() must not call POSIX-only os.getpgid on Windows.""" + with mock_launcher_deps(tmp_path, ready=True) as mocks: + launcher = CloudXRLauncher() + proc = mocks["proc"] + poll_seq = [None, 0] + proc.poll = MagicMock( + side_effect=lambda: poll_seq.pop(0) if poll_seq else 0 + ) + + with patch("isaacteleop.cloudxr.launcher.sys.platform", "win32"): + launcher.stop() + + proc.terminate.assert_called_once() + + if __name__ == "__main__": pytest.main([__file__, "-v"]) From 84970eaf5c6624b03811a4f5e7c4a81cc35bd41e Mon Sep 17 00:00:00 2001 From: Jakob Bornecrantz Date: Wed, 24 Jun 2026 15:39:58 +0100 Subject: [PATCH 2/2] Launch CloudXR in-process across live OpenXR examples. Wrap examples that need a running CloudXR runtime with CloudXRLauncher, using the new --launch-cloudxr-runtime / --no-launch-cloudxr-runtime flags where argparse is already present. Skips synthetic-plugin and mcap replay examples that do not require a live runtime. Signed-off-by: Jakob Bornecrantz --- .../python/controller_haptic_example.py | 38 +- examples/lerobot/record.py | 218 ++++++----- examples/oxr/python/modular_example.py | 190 ++++----- .../oxr/python/modular_example_with_mcap.py | 128 +++--- .../python/dual_source_teleop_example.py | 272 ++++++------- .../python/sharpa_hand_retargeter_demo.py | 9 +- .../retargeting/python/sources_example.py | 368 +++++++++--------- .../teleop/python/dex_bimanual_example.py | 79 ++-- .../full_bimanual_reordering_example.py | 125 +++--- .../python/isaac_lab_gripper_example.py | 30 +- .../python/joint_space_device_example.py | 17 +- .../teleop/python/se3_retargeting_example.py | 44 ++- .../python/message_channel_example.py | 61 +-- .../python/teleop_controls_simple_example.py | 99 ++--- 14 files changed, 867 insertions(+), 811 deletions(-) diff --git a/examples/haptic_feedback/python/controller_haptic_example.py b/examples/haptic_feedback/python/controller_haptic_example.py index f070e4c71..a93756efa 100644 --- a/examples/haptic_feedback/python/controller_haptic_example.py +++ b/examples/haptic_feedback/python/controller_haptic_example.py @@ -58,6 +58,7 @@ ControllerInputIndex, TactileVector, ) +from isaacteleop.cloudxr import CloudXRLauncher from isaacteleop.teleop_session_manager import TeleopSession, TeleopSessionConfig @@ -159,24 +160,25 @@ def main() -> None: print("Press Ctrl+C to exit.\n") frame_period_s = 1.0 / FPS - with TeleopSession(config) as session: - while True: - result = session.step() - trig_l, trig_r = ( - _scalar(result, "trigger_left"), - _scalar(result, "trigger_right"), - ) - hap_l, hap_r = ( - _amplitude(result, "haptic_left"), - _amplitude(result, "haptic_right"), - ) - line = ( - f"L trig {_bar(trig_l)} {trig_l:.2f} -> L rumble {_bar(hap_l)} {hap_l:.2f}" - " | " - f"R trig {_bar(trig_r)} {trig_r:.2f} -> R rumble {_bar(hap_r)} {hap_r:.2f}" - ) - print(f"\r{line:<96}", end="", flush=True) - time.sleep(frame_period_s) + with CloudXRLauncher(): + with TeleopSession(config) as session: + while True: + result = session.step() + trig_l, trig_r = ( + _scalar(result, "trigger_left"), + _scalar(result, "trigger_right"), + ) + hap_l, hap_r = ( + _amplitude(result, "haptic_left"), + _amplitude(result, "haptic_right"), + ) + line = ( + f"L trig {_bar(trig_l)} {trig_l:.2f} -> L rumble {_bar(hap_l)} {hap_l:.2f}" + " | " + f"R trig {_bar(trig_r)} {trig_r:.2f} -> R rumble {_bar(hap_r)} {hap_r:.2f}" + ) + print(f"\r{line:<96}", end="", flush=True) + time.sleep(frame_period_s) if __name__ == "__main__": diff --git a/examples/lerobot/record.py b/examples/lerobot/record.py index 8dace6ec3..c6b78bd53 100644 --- a/examples/lerobot/record.py +++ b/examples/lerobot/record.py @@ -16,6 +16,7 @@ import isaacteleop.deviceio as deviceio import isaacteleop.oxr as oxr import isaacteleop.schema as schema +from isaacteleop.cloudxr import CloudXRLauncher from lerobot.datasets.lerobot_dataset import LeRobotDataset @@ -86,113 +87,118 @@ def main(): # Create OpenXR session print("\nCreating OpenXR session...") - with oxr.OpenXRSession("ModularExample", required_extensions) as oxr_session: - handles = oxr_session.get_handles() - print("OpenXR session created") - - # Create teleop session - print("\nInitializing teleop session...") - session = deviceio.DeviceIOSession.run(trackers, handles) - - with session: - print("Teleop session initialized with all trackers!") - print() - - # Main tracking loop - print("===========================================") - print("Tracking + Recording (60 seconds)...") - print("===========================================") - print() - - frame_count = 0 - start_time = time.time() - - try: - while time.time() - start_time < 10.0: - # Update session and all trackers - session.update() - - # Get hand data - left_tracked: schema.HandPoseTrackedT = hand_tracker.get_left_hand( - session - ) - right_tracked: schema.HandPoseTrackedT = ( - hand_tracker.get_right_hand(session) - ) - head_tracked: schema.HeadPoseTrackedT = head_tracker.get_head( - session - ) - - # Extract positions and orientations (with defaults for invalid data) - left_pos = np.zeros(3, dtype=np.float32) - right_pos = np.zeros(3, dtype=np.float32) - - if left_tracked.data is not None and left_tracked.data.joints: - wrist = left_tracked.data.joints.poses(deviceio.JOINT_WRIST) - if wrist.is_valid: - pos = wrist.pose.position - left_pos = np.array([pos.x, pos.y, pos.z], dtype=np.float32) - - if right_tracked.data is not None and right_tracked.data.joints: - wrist = right_tracked.data.joints.poses(deviceio.JOINT_WRIST) - if wrist.is_valid: - pos = wrist.pose.position - right_pos = np.array( - [pos.x, pos.y, pos.z], dtype=np.float32 + with CloudXRLauncher(): + with oxr.OpenXRSession("ModularExample", required_extensions) as oxr_session: + handles = oxr_session.get_handles() + print("OpenXR session created") + + # Create teleop session + print("\nInitializing teleop session...") + session = deviceio.DeviceIOSession.run(trackers, handles) + + with session: + print("Teleop session initialized with all trackers!") + print() + + # Main tracking loop + print("===========================================") + print("Tracking + Recording (60 seconds)...") + print("===========================================") + print() + + frame_count = 0 + start_time = time.time() + + try: + while time.time() - start_time < 10.0: + # Update session and all trackers + session.update() + + # Get hand data + left_tracked: schema.HandPoseTrackedT = ( + hand_tracker.get_left_hand(session) + ) + right_tracked: schema.HandPoseTrackedT = ( + hand_tracker.get_right_hand(session) + ) + head_tracked: schema.HeadPoseTrackedT = head_tracker.get_head( + session + ) + + # Extract positions and orientations (with defaults for invalid data) + left_pos = np.zeros(3, dtype=np.float32) + right_pos = np.zeros(3, dtype=np.float32) + + if left_tracked.data is not None and left_tracked.data.joints: + wrist = left_tracked.data.joints.poses(deviceio.JOINT_WRIST) + if wrist.is_valid: + pos = wrist.pose.position + left_pos = np.array( + [pos.x, pos.y, pos.z], dtype=np.float32 + ) + + if right_tracked.data is not None and right_tracked.data.joints: + wrist = right_tracked.data.joints.poses( + deviceio.JOINT_WRIST ) - - head_pos = np.zeros(3, dtype=np.float32) - if head_tracked.data is not None and head_tracked.data.is_valid: - pos = head_tracked.data.pose.position - head_pos = np.array([pos.x, pos.y, pos.z], dtype=np.float32) - - # STEP 3: Record frame to dataset - observation_head = np.concatenate( - [ - head_pos, # head_pos(3) - ] - ) - - observation_left_hand = np.concatenate( - [ - left_pos, # left_hand_pos(3) - ] - ) - - observation_right_hand = np.concatenate( - [ - right_pos, # right_hand_pos(3) - ] - ) - - dataset.add_frame( - { - "task": "teleop_tracking", - "observation.head": observation_head, - "observation.left_hand": observation_left_hand, - "observation.right_hand": observation_right_hand, - } - ) - - # Print every 60 frames (~1 second) - if frame_count % 60 == 0: - elapsed = time.time() - start_time - print(f"[{elapsed:4.1f}s] Frame {frame_count} recorded") - - frame_count += 1 - time.sleep(0.016) # ~60 FPS - except KeyboardInterrupt: - print("\nKeyboardInterrupt received, stopping recording early.") - - # STEP 4: Save episode - print(f"\nSaving episode with {frame_count} frames...") - dataset.save_episode() - print("Episode saved") - - # Cleanup - print(f"\nProcessed {frame_count} frames") - print("Cleaning up (RAII)...") - print("Resources will be cleaned up when exiting 'with' blocks") + if wrist.is_valid: + pos = wrist.pose.position + right_pos = np.array( + [pos.x, pos.y, pos.z], dtype=np.float32 + ) + + head_pos = np.zeros(3, dtype=np.float32) + if head_tracked.data is not None and head_tracked.data.is_valid: + pos = head_tracked.data.pose.position + head_pos = np.array([pos.x, pos.y, pos.z], dtype=np.float32) + + # STEP 3: Record frame to dataset + observation_head = np.concatenate( + [ + head_pos, # head_pos(3) + ] + ) + + observation_left_hand = np.concatenate( + [ + left_pos, # left_hand_pos(3) + ] + ) + + observation_right_hand = np.concatenate( + [ + right_pos, # right_hand_pos(3) + ] + ) + + dataset.add_frame( + { + "task": "teleop_tracking", + "observation.head": observation_head, + "observation.left_hand": observation_left_hand, + "observation.right_hand": observation_right_hand, + } + ) + + # Print every 60 frames (~1 second) + if frame_count % 60 == 0: + elapsed = time.time() - start_time + print(f"[{elapsed:4.1f}s] Frame {frame_count} recorded") + + frame_count += 1 + time.sleep(0.016) # ~60 FPS + except KeyboardInterrupt: + print("\nKeyboardInterrupt received, stopping recording early.") + + # STEP 4: Save episode + print(f"\nSaving episode with {frame_count} frames...") + dataset.save_episode() + print("Episode saved") + + # Cleanup + print(f"\nProcessed {frame_count} frames") + print("Cleaning up (RAII)...") + print("Resources will be cleaned up when exiting 'with' blocks") # STEP 5: Finalize dataset (creates stats.json) print("\nFinalizing dataset...") diff --git a/examples/oxr/python/modular_example.py b/examples/oxr/python/modular_example.py index fa1f7b6a2..71383bb7b 100755 --- a/examples/oxr/python/modular_example.py +++ b/examples/oxr/python/modular_example.py @@ -16,106 +16,108 @@ import isaacteleop.deviceio as deviceio import isaacteleop.oxr as oxr import isaacteleop.schema as schema +from isaacteleop.cloudxr import CloudXRLauncher def main(): - print("=" * 60) - print("OpenXR Modular Tracking Example") - print("=" * 60) - print() - - # Create trackers independently - print("Creating trackers...") - hand_tracker = deviceio.HandTracker() - head_tracker = deviceio.HeadTracker() - print(f"✓ Created {hand_tracker.get_name()}") - print(f"✓ Created {head_tracker.get_name()}") - - # Get required extensions - print("\nQuerying required extensions...") - trackers = [hand_tracker, head_tracker] - required_extensions = deviceio.DeviceIOSession.get_required_extensions(trackers) - print(f"✓ Required extensions: {required_extensions}") - - # Create OpenXR session - print("\nCreating OpenXR session...") - with oxr.OpenXRSession("ModularExample", required_extensions) as oxr_session: - handles = oxr_session.get_handles() - print("✓ OpenXR session created") - - # Run deviceio session with trackers (throws exception on failure) - print("\nRunning deviceio session with trackers...") - with deviceio.DeviceIOSession.run(trackers, handles) as session: - print("✓ DeviceIO session initialized with all trackers!") - print() - - # Main tracking loop - print("=" * 60) - print("Tracking (10 seconds)...") - print("=" * 60) - print() - - frame_count = 0 - start_time = time.time() - - while time.time() - start_time < 10.0: - # Update session and all trackers - session.update() - - # Print every 60 frames (~1 second) - if frame_count % 60 == 0: - elapsed = time.time() - start_time - print(f"[{elapsed:4.1f}s] Frame {frame_count}") - - # Get hand data - left_tracked: schema.HandPoseTrackedT = hand_tracker.get_left_hand( - session - ) - right_tracked: schema.HandPoseTrackedT = ( - hand_tracker.get_right_hand(session) - ) - - if left_tracked.data is not None: - pos = left_tracked.data.joints.poses( - deviceio.JOINT_WRIST - ).pose.position - print( - f" Left wrist: [{pos.x:6.3f}, {pos.y:6.3f}, {pos.z:6.3f}]" + with CloudXRLauncher(): + print("=" * 60) + print("OpenXR Modular Tracking Example") + print("=" * 60) + print() + + # Create trackers independently + print("Creating trackers...") + hand_tracker = deviceio.HandTracker() + head_tracker = deviceio.HeadTracker() + print(f"✓ Created {hand_tracker.get_name()}") + print(f"✓ Created {head_tracker.get_name()}") + + # Get required extensions + print("\nQuerying required extensions...") + trackers = [hand_tracker, head_tracker] + required_extensions = deviceio.DeviceIOSession.get_required_extensions(trackers) + print(f"✓ Required extensions: {required_extensions}") + + # Create OpenXR session + print("\nCreating OpenXR session...") + with oxr.OpenXRSession("ModularExample", required_extensions) as oxr_session: + handles = oxr_session.get_handles() + print("✓ OpenXR session created") + + # Run deviceio session with trackers (throws exception on failure) + print("\nRunning deviceio session with trackers...") + with deviceio.DeviceIOSession.run(trackers, handles) as session: + print("✓ DeviceIO session initialized with all trackers!") + print() + + # Main tracking loop + print("=" * 60) + print("Tracking (10 seconds)...") + print("=" * 60) + print() + + frame_count = 0 + start_time = time.time() + + while time.time() - start_time < 10.0: + # Update session and all trackers + session.update() + + # Print every 60 frames (~1 second) + if frame_count % 60 == 0: + elapsed = time.time() - start_time + print(f"[{elapsed:4.1f}s] Frame {frame_count}") + + # Get hand data + left_tracked: schema.HandPoseTrackedT = ( + hand_tracker.get_left_hand(session) ) - else: - print(" Left hand: inactive") - - if right_tracked.data is not None: - pos = right_tracked.data.joints.poses( - deviceio.JOINT_WRIST - ).pose.position - print( - f" Right wrist: [{pos.x:6.3f}, {pos.y:6.3f}, {pos.z:6.3f}]" + right_tracked: schema.HandPoseTrackedT = ( + hand_tracker.get_right_hand(session) ) - else: - print(" Right hand: inactive") - - # Get head data - head_tracked: schema.HeadPoseTrackedT = head_tracker.get_head( - session - ) - if head_tracked.data is not None: - pos = head_tracked.data.pose.position - print( - f" Head pos: [{pos.x:6.3f}, {pos.y:6.3f}, {pos.z:6.3f}]" - ) - else: - print(" Head: inactive") - - print() - frame_count += 1 - time.sleep(0.016) # ~60 FPS - - # Cleanup - print(f"\nProcessed {frame_count} frames") - print("Cleaning up (RAII)...") - print("✓ Resources will be cleaned up when exiting 'with' blocks") + if left_tracked.data is not None: + pos = left_tracked.data.joints.poses( + deviceio.JOINT_WRIST + ).pose.position + print( + f" Left wrist: [{pos.x:6.3f}, {pos.y:6.3f}, {pos.z:6.3f}]" + ) + else: + print(" Left hand: inactive") + + if right_tracked.data is not None: + pos = right_tracked.data.joints.poses( + deviceio.JOINT_WRIST + ).pose.position + print( + f" Right wrist: [{pos.x:6.3f}, {pos.y:6.3f}, {pos.z:6.3f}]" + ) + else: + print(" Right hand: inactive") + + # Get head data + head_tracked: schema.HeadPoseTrackedT = head_tracker.get_head( + session + ) + if head_tracked.data is not None: + pos = head_tracked.data.pose.position + print( + f" Head pos: [{pos.x:6.3f}, {pos.y:6.3f}, {pos.z:6.3f}]" + ) + else: + print(" Head: inactive") + + print() + + frame_count += 1 + time.sleep(0.016) # ~60 FPS + + # Cleanup + print(f"\nProcessed {frame_count} frames") + print("Cleaning up (RAII)...") + print("✓ Resources will be cleaned up when exiting 'with' blocks") print("Done!") return 0 diff --git a/examples/oxr/python/modular_example_with_mcap.py b/examples/oxr/python/modular_example_with_mcap.py index d766a94ec..54b03b4f1 100644 --- a/examples/oxr/python/modular_example_with_mcap.py +++ b/examples/oxr/python/modular_example_with_mcap.py @@ -16,6 +16,7 @@ from datetime import datetime import isaacteleop.deviceio as deviceio import isaacteleop.oxr as oxr +from isaacteleop.cloudxr import CloudXRLauncher RECORD_DURATION_S = 10.0 @@ -31,76 +32,79 @@ def main(): timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") mcap_filename = f"tracking_recording_{timestamp}.mcap" - # Create trackers independently - print("Creating trackers...") - hand_tracker = deviceio.HandTracker() - head_tracker = deviceio.HeadTracker() - print(f"✓ Created {hand_tracker.get_name()}") - print(f"✓ Created {head_tracker.get_name()}") - - # Get required extensions - print("\nQuerying required extensions...") - trackers = [hand_tracker, head_tracker] - required_extensions = deviceio.DeviceIOSession.get_required_extensions(trackers) - print(f"✓ Required extensions: {required_extensions}") - - # Create OpenXR session - print("\nCreating OpenXR session...") - with oxr.OpenXRSession( - "ModularExampleWithMCAP", required_extensions - ) as oxr_session: - handles = oxr_session.get_handles() - print("✓ OpenXR session created") - - # Run deviceio session with MCAP recording enabled. - print("\nRunning deviceio session with MCAP recording...") - recording_config = deviceio.McapRecordingConfig( - mcap_filename, [(hand_tracker, "hands"), (head_tracker, "head")] - ) - with deviceio.DeviceIOSession.run( - trackers, handles, recording_config - ) as session: - print("✓ DeviceIO session initialized with all trackers!") - print(f"✓ MCAP recording active → {mcap_filename}") - print() - - # Main tracking loop - print("=" * 60) - print(f"Tracking ({RECORD_DURATION_S} seconds)...") - print("=" * 60) - print() - - frame_count = 0 - start_time = time.time() - - while time.time() - start_time < RECORD_DURATION_S: - session.update() - - # Print every 60 frames (~1 second) - if frame_count % 60 == 0: - elapsed = time.time() - start_time - print(f"[{elapsed:4.1f}s] Frame {frame_count} (recording...)") - print() - - frame_count += 1 - time.sleep(0.016) # ~60 FPS - - print(f"\nProcessed {frame_count} frames") - - print("✓ Recording stopped (MCAP file closed by session destructor)") + with CloudXRLauncher(): + # Create trackers independently + print("Creating trackers...") + hand_tracker = deviceio.HandTracker() + head_tracker = deviceio.HeadTracker() + print(f"✓ Created {hand_tracker.get_name()}") + print(f"✓ Created {head_tracker.get_name()}") + + # Get required extensions + print("\nQuerying required extensions...") + trackers = [hand_tracker, head_tracker] + required_extensions = deviceio.DeviceIOSession.get_required_extensions(trackers) + print(f"✓ Required extensions: {required_extensions}") + + # Create OpenXR session + print("\nCreating OpenXR session...") + with oxr.OpenXRSession( + "ModularExampleWithMCAP", required_extensions + ) as oxr_session: + handles = oxr_session.get_handles() + print("✓ OpenXR session created") + + # Run deviceio session with MCAP recording enabled. + print("\nRunning deviceio session with MCAP recording...") + recording_config = deviceio.McapRecordingConfig( + mcap_filename, [(hand_tracker, "hands"), (head_tracker, "head")] + ) + with deviceio.DeviceIOSession.run( + trackers, handles, recording_config + ) as session: + print("✓ DeviceIO session initialized with all trackers!") + print(f"✓ MCAP recording active → {mcap_filename}") + print() - print() - print("=" * 60) - print(f"✓ Recording saved to: {mcap_filename}") - print("=" * 60) + # Main tracking loop + print("=" * 60) + print(f"Tracking ({RECORD_DURATION_S} seconds)...") + print("=" * 60) + print() + + frame_count = 0 + start_time = time.time() + + while time.time() - start_time < RECORD_DURATION_S: + session.update() - # ---- Replay the recorded MCAP file ---- + # Print every 60 frames (~1 second) + if frame_count % 60 == 0: + elapsed = time.time() - start_time + print(f"[{elapsed:4.1f}s] Frame {frame_count} (recording...)") + print() + + frame_count += 1 + time.sleep(0.016) # ~60 FPS + + print(f"\nProcessed {frame_count} frames") + + print("✓ Recording stopped (MCAP file closed by session destructor)") + + print() + print("=" * 60) + print(f"✓ Recording saved to: {mcap_filename}") + print("=" * 60) + + # ---- Replay the recorded MCAP file (no live OpenXR session required) ---- print() print("=" * 60) print("Replaying recorded MCAP data") print("=" * 60) print() + hand_tracker = deviceio.HandTracker() + head_tracker = deviceio.HeadTracker() replay_config = deviceio.McapReplayConfig( mcap_filename, [(hand_tracker, "hands"), (head_tracker, "head")] ) diff --git a/examples/retargeting/python/dual_source_teleop_example.py b/examples/retargeting/python/dual_source_teleop_example.py index 9730106a8..c514cb506 100644 --- a/examples/retargeting/python/dual_source_teleop_example.py +++ b/examples/retargeting/python/dual_source_teleop_example.py @@ -29,6 +29,7 @@ import sys import time +from isaacteleop.cloudxr import CloudXRLauncher from isaacteleop.retargeting_engine.deviceio_source_nodes import ( ControllersSource, ) @@ -44,148 +45,153 @@ def main() -> int: - print("=" * 80) - print(" Dual ControllersSource Teleop Example") - print("=" * 80) - print("Uses two ControllersSource nodes in a single pipeline.") - print("Each source feeds one SE3 retargeter for bimanual arm control.") - print("=" * 80 + "\n") - - # ================================================================== - # Step 1: Create two separate ControllersSource nodes - # ================================================================== - # Each ControllersSource creates its own ControllerTracker internally. - # BUG: TeleopSession deduplicates trackers by type, so only the first - # source's tracker will be registered with DeviceIO. The second - # source's tracker is orphaned. - - print("[Step 1] Creating two ControllersSource nodes...") - controller_left_source = ControllersSource(name="controller_left") - controller_right_source = ControllersSource(name="controller_right") - print( - f" ✓ ControllersSource('controller_left') - tracker id: {id(controller_left_source.get_tracker())}" - ) - print( - f" ✓ ControllersSource('controller_right') - tracker id: {id(controller_right_source.get_tracker())}" - ) - print( - f" ⚠ Both trackers are type: {type(controller_left_source.get_tracker()).__name__}" - ) - print(" Only one will survive deduplication in TeleopSession.__enter__()") - - # ================================================================== - # Step 2: Build retargeting pipeline - # ================================================================== - # Left arm: controller_left from first source - # Right arm: controller_right from second source - - print("\n[Step 2] Building retargeting pipeline...") - - # Left arm SE3 retargeter (using left controller from first source) - left_se3_config = Se3RetargeterConfig( - input_device=ControllersSource.LEFT, - use_wrist_position=True, - use_wrist_rotation=True, - zero_out_xy_rotation=False, - ) - left_se3 = Se3AbsRetargeter(left_se3_config, name="left_se3") - connected_left = left_se3.connect( - { - ControllersSource.LEFT: controller_left_source.output( - ControllersSource.LEFT - ), - } - ) - print(" ✓ Left SE3: controller_left_source.controller_left -> left_ee_pose") - - # Right arm SE3 retargeter (using right controller from second source) - right_se3_config = Se3RetargeterConfig( - input_device=ControllersSource.RIGHT, - use_wrist_position=True, - use_wrist_rotation=True, - zero_out_xy_rotation=False, - ) - right_se3 = Se3AbsRetargeter(right_se3_config, name="right_se3") - connected_right = right_se3.connect( - { - ControllersSource.RIGHT: controller_right_source.output( - ControllersSource.RIGHT - ), - } - ) - print(" ✓ Right SE3: controller_right_source.controller_right -> right_ee_pose") - - # ================================================================== - # Step 3: Combine outputs - # ================================================================== - print("\n[Step 3] Combining outputs...") - - pipeline = OutputCombiner( - { - "left_ee_pose": connected_left.output("ee_pose"), - "right_ee_pose": connected_right.output("ee_pose"), - } - ) - print(" ✓ Pipeline: 2 ControllersSource -> 2 Se3AbsRetargeter -> OutputCombiner") - - # ================================================================== - # Step 4: Create and run TeleopSession - # ================================================================== - print("\n[Step 4] Creating TeleopSession...") - - session_config = TeleopSessionConfig( - app_name="DualControllerSourceExample", - trackers=[], # Auto-discovered from pipeline sources - pipeline=pipeline, - ) - - with TeleopSession(session_config) as session: - print(" ✓ Session initialized") - - # Diagnostic: show which tracker survived deduplication - print("\n [Diagnostic] Discovered sources:") - for source in session._sources: - tracker = source.get_tracker() - print( - f" - {source.name}: tracker id={id(tracker)}, type={type(tracker).__name__}" - ) - - print("\n" + "=" * 80) - print(" Running Bimanual Controller Teleop (20 seconds)") - print(" Move left/right controllers to position arms") + with CloudXRLauncher(): + print("=" * 80) + print(" Dual ControllersSource Teleop Example") + print("=" * 80) + print("Uses two ControllersSource nodes in a single pipeline.") + print("Each source feeds one SE3 retargeter for bimanual arm control.") print("=" * 80 + "\n") - start_time = time.time() - duration = 20.0 + # ================================================================== + # Step 1: Create two separate ControllersSource nodes + # ================================================================== + # Each ControllersSource creates its own ControllerTracker internally. + # BUG: TeleopSession deduplicates trackers by type, so only the first + # source's tracker will be registered with DeviceIO. The second + # source's tracker is orphaned. + + print("[Step 1] Creating two ControllersSource nodes...") + controller_left_source = ControllersSource(name="controller_left") + controller_right_source = ControllersSource(name="controller_right") + print( + f" ✓ ControllersSource('controller_left') - tracker id: {id(controller_left_source.get_tracker())}" + ) + print( + f" ✓ ControllersSource('controller_right') - tracker id: {id(controller_right_source.get_tracker())}" + ) + print( + f" ⚠ Both trackers are type: {type(controller_left_source.get_tracker()).__name__}" + ) + print(" Only one will survive deduplication in TeleopSession.__enter__()") + + # ================================================================== + # Step 2: Build retargeting pipeline + # ================================================================== + # Left arm: controller_left from first source + # Right arm: controller_right from second source + + print("\n[Step 2] Building retargeting pipeline...") + + # Left arm SE3 retargeter (using left controller from first source) + left_se3_config = Se3RetargeterConfig( + input_device=ControllersSource.LEFT, + use_wrist_position=True, + use_wrist_rotation=True, + zero_out_xy_rotation=False, + ) + left_se3 = Se3AbsRetargeter(left_se3_config, name="left_se3") + connected_left = left_se3.connect( + { + ControllersSource.LEFT: controller_left_source.output( + ControllersSource.LEFT + ), + } + ) + print(" ✓ Left SE3: controller_left_source.controller_left -> left_ee_pose") + + # Right arm SE3 retargeter (using right controller from second source) + right_se3_config = Se3RetargeterConfig( + input_device=ControllersSource.RIGHT, + use_wrist_position=True, + use_wrist_rotation=True, + zero_out_xy_rotation=False, + ) + right_se3 = Se3AbsRetargeter(right_se3_config, name="right_se3") + connected_right = right_se3.connect( + { + ControllersSource.RIGHT: controller_right_source.output( + ControllersSource.RIGHT + ), + } + ) + print( + " ✓ Right SE3: controller_right_source.controller_right -> right_ee_pose" + ) + + # ================================================================== + # Step 3: Combine outputs + # ================================================================== + print("\n[Step 3] Combining outputs...") + + pipeline = OutputCombiner( + { + "left_ee_pose": connected_left.output("ee_pose"), + "right_ee_pose": connected_right.output("ee_pose"), + } + ) + print( + " ✓ Pipeline: 2 ControllersSource -> 2 Se3AbsRetargeter -> OutputCombiner" + ) + + # ================================================================== + # Step 4: Create and run TeleopSession + # ================================================================== + print("\n[Step 4] Creating TeleopSession...") + + session_config = TeleopSessionConfig( + app_name="DualControllerSourceExample", + trackers=[], # Auto-discovered from pipeline sources + pipeline=pipeline, + ) + + with TeleopSession(session_config) as session: + print(" ✓ Session initialized") + + # Diagnostic: show which tracker survived deduplication + print("\n [Diagnostic] Discovered sources:") + for source in session._sources: + tracker = source.get_tracker() + print( + f" - {source.name}: tracker id={id(tracker)}, type={type(tracker).__name__}" + ) - while time.time() - start_time < duration: - # BUG: This will fail or produce incorrect results for the second source. - # The second ControllersSource polls its own tracker, which was never - # registered with DeviceIO (discarded during deduplication). - result = session.step() + print("\n" + "=" * 80) + print(" Running Bimanual Controller Teleop (20 seconds)") + print(" Move left/right controllers to position arms") + print("=" * 80 + "\n") - left_pose = result["left_ee_pose"][0] - right_pose = result["right_ee_pose"][0] + start_time = time.time() + duration = 20.0 - if session.frame_count % 30 == 0: - elapsed = session.get_elapsed_time() - left_pos = left_pose[:3] - right_pos = right_pose[:3] + while time.time() - start_time < duration: + # BUG: This will fail or produce incorrect results for the second source. + # The second ControllersSource polls its own tracker, which was never + # registered with DeviceIO (discarded during deduplication). + result = session.step() - print(f"[{elapsed:5.1f}s] Frame {session.frame_count}") - print( - f" Left arm: ({left_pos[0]:+6.3f}, {left_pos[1]:+6.3f}, {left_pos[2]:+6.3f})" - ) - print( - f" Right arm: ({right_pos[0]:+6.3f}, {right_pos[1]:+6.3f}, {right_pos[2]:+6.3f})" - ) + left_pose = result["left_ee_pose"][0] + right_pose = result["right_ee_pose"][0] + + if session.frame_count % 30 == 0: + elapsed = session.get_elapsed_time() + left_pos = left_pose[:3] + right_pos = right_pose[:3] + + print(f"[{elapsed:5.1f}s] Frame {session.frame_count}") + print( + f" Left arm: ({left_pos[0]:+6.3f}, {left_pos[1]:+6.3f}, {left_pos[2]:+6.3f})" + ) + print( + f" Right arm: ({right_pos[0]:+6.3f}, {right_pos[1]:+6.3f}, {right_pos[2]:+6.3f})" + ) - time.sleep(0.016) + time.sleep(0.016) - fps = session.frame_count / duration - print(f"\n Done. Processed {session.frame_count} frames ({fps:.1f} FPS)") + fps = session.frame_count / duration + print(f"\n Done. Processed {session.frame_count} frames ({fps:.1f} FPS)") - print("\n✅ Example completed successfully!") + print("\n✅ Example completed successfully!") return 0 diff --git a/examples/retargeting/python/sharpa_hand_retargeter_demo.py b/examples/retargeting/python/sharpa_hand_retargeter_demo.py index f75a7df4c..11b54d711 100755 --- a/examples/retargeting/python/sharpa_hand_retargeter_demo.py +++ b/examples/retargeting/python/sharpa_hand_retargeter_demo.py @@ -29,6 +29,8 @@ import sys import time +from isaacteleop.cloudxr import CloudXRLauncher + import numpy as np from isaacteleop.retargeters import SharpaHandRetargeter, SharpaHandRetargeterConfig @@ -326,6 +328,7 @@ def main(): default=60.0, help="Duration in seconds for the live session (default: 60).", ) + CloudXRLauncher.add_launcher_arguments(parser) args = parser.parse_args() print() @@ -338,9 +341,9 @@ def main(): print(" Mode: SYNTHETIC (no headset)") print() return _run_synthetic(args.right_mjcf) - else: - print(" Mode: LIVE (Quest hand tracking)") - print() + print(" Mode: LIVE (Quest hand tracking)") + print() + with CloudXRLauncher.launch_context(args): return _run_live(args.left_mjcf, args.right_mjcf, args.duration) diff --git a/examples/retargeting/python/sources_example.py b/examples/retargeting/python/sources_example.py index 602db91d8..b7cabc5aa 100755 --- a/examples/retargeting/python/sources_example.py +++ b/examples/retargeting/python/sources_example.py @@ -24,6 +24,7 @@ import time import isaacteleop.deviceio as deviceio import isaacteleop.oxr as oxr +from isaacteleop.cloudxr import CloudXRLauncher from isaacteleop.retargeting_engine.deviceio_source_nodes import ( HandsSource, HeadSource, @@ -72,196 +73,199 @@ def main(): # Step 3: Create OpenXR session # ======================================================================== print("\n[Step 4] Creating OpenXR session...") - with oxr.OpenXRSession( - "RetargetingSourcesExample", required_extensions - ) as oxr_session: - handles = oxr_session.get_handles() - print(" ✓ OpenXR session created successfully") - - # ==================================================================== - # Step 5: Run DeviceIO session - # ==================================================================== - print("\n[Step 5] Initializing DeviceIO session...") - with deviceio.DeviceIOSession.run(trackers, handles) as session: - print(" ✓ DeviceIO session initialized with all trackers") - - # ================================================================ - # Step 6: Create OutputCombiner to combine all source outputs - # ================================================================ - print("\n[Step 6] Creating OutputCombiner to combine all sources...") - combiner = OutputCombiner( - { - # Hand outputs - HandsSource.LEFT: hands_source.output(HandsSource.LEFT), - HandsSource.RIGHT: hands_source.output(HandsSource.RIGHT), - # Head output - "head": head_source.output("head"), - # Controller outputs - ControllersSource.LEFT: controllers_source.output( - ControllersSource.LEFT - ), - ControllersSource.RIGHT: controllers_source.output( - ControllersSource.RIGHT - ), - } - ) - print(" ✓ Created OutputCombiner with 5 combined outputs") - - # ================================================================ - # Step 7: Main tracking loop - # ================================================================ - print("\n[Step 7] Starting main tracking loop...") - print("=" * 70) - print("Tracking Data (10 seconds)") - print("=" * 70) - print() - - frame_count = 0 - start_time = time.time() - - while time.time() - start_time < 10.0: - # Update session and all trackers - session.update() - - # Print every 60 frames (~1 second) - if frame_count % 60 == 0: - elapsed = time.time() - start_time - print(f"[{elapsed:4.1f}s] Frame {frame_count}") - print("-" * 70) - - # ==================================================== - # Manually poll DeviceIO trackers for tracked objects - # ==================================================== - hand_left_tracked = hand_tracker.get_left_hand(session) - hand_right_tracked = hand_tracker.get_right_hand(session) - head_tracked = head_tracker.get_head(session) - left_ctrl_tracked = controller_tracker.get_left_controller(session) - right_ctrl_tracked = controller_tracker.get_right_controller( - session - ) - - # ==================================================== - # Wrap tracked objects in TensorGroups for source inputs - # ==================================================== - - hands_inputs = {} - hands_input_spec = hands_source.input_spec() - for input_name, group_type in hands_input_spec.items(): - tg = TensorGroup(group_type) - if "left" in input_name.lower(): - tg[0] = hand_left_tracked - elif "right" in input_name.lower(): - tg[0] = hand_right_tracked - hands_inputs[input_name] = tg - - head_inputs = {} - head_input_spec = head_source.input_spec() - for input_name, group_type in head_input_spec.items(): - tg = TensorGroup(group_type) - tg[0] = head_tracked - head_inputs[input_name] = tg - - controllers_inputs = {} - controllers_input_spec = controllers_source.input_spec() - for input_name, group_type in controllers_input_spec.items(): - tg = TensorGroup(group_type) - if "left" in input_name.lower(): - tg[0] = left_ctrl_tracked - elif "right" in input_name.lower(): - tg[0] = right_ctrl_tracked - controllers_inputs[input_name] = tg - - # ==================================================== - # Pass wrapped data to combiner with correct structure - # ==================================================== - all_data = combiner( - { - "hands": hands_inputs, - "head": head_inputs, - "controllers": controllers_inputs, - } - ) - - # Extract hand data (now in tensor format) - # Source nodes emit None when tracking is inactive (Optional). - left_hand = all_data[HandsSource.LEFT] - right_hand = all_data[HandsSource.RIGHT] - - print(" Hands:") - print( - f" Left: {'ACTIVE' if not left_hand.is_none else 'INACTIVE'}" - ) - if not left_hand.is_none: - left_positions = left_hand[HandInputIndex.JOINT_POSITIONS] - wrist_idx = deviceio.JOINT_WRIST - wrist_pos = left_positions[wrist_idx] - print( - f" Wrist: [{wrist_pos[0]:6.3f}, {wrist_pos[1]:6.3f}, {wrist_pos[2]:6.3f}]" + with CloudXRLauncher(): + with oxr.OpenXRSession( + "RetargetingSourcesExample", required_extensions + ) as oxr_session: + handles = oxr_session.get_handles() + print(" ✓ OpenXR session created successfully") + + # ==================================================================== + # Step 5: Run DeviceIO session + # ==================================================================== + print("\n[Step 5] Initializing DeviceIO session...") + with deviceio.DeviceIOSession.run(trackers, handles) as session: + print(" ✓ DeviceIO session initialized with all trackers") + + # ================================================================ + # Step 6: Create OutputCombiner to combine all source outputs + # ================================================================ + print("\n[Step 6] Creating OutputCombiner to combine all sources...") + combiner = OutputCombiner( + { + # Hand outputs + HandsSource.LEFT: hands_source.output(HandsSource.LEFT), + HandsSource.RIGHT: hands_source.output(HandsSource.RIGHT), + # Head output + "head": head_source.output("head"), + # Controller outputs + ControllersSource.LEFT: controllers_source.output( + ControllersSource.LEFT + ), + ControllersSource.RIGHT: controllers_source.output( + ControllersSource.RIGHT + ), + } + ) + print(" ✓ Created OutputCombiner with 5 combined outputs") + + # ================================================================ + # Step 7: Main tracking loop + # ================================================================ + print("\n[Step 7] Starting main tracking loop...") + print("=" * 70) + print("Tracking Data (10 seconds)") + print("=" * 70) + print() + + frame_count = 0 + start_time = time.time() + + while time.time() - start_time < 10.0: + # Update session and all trackers + session.update() + + # Print every 60 frames (~1 second) + if frame_count % 60 == 0: + elapsed = time.time() - start_time + print(f"[{elapsed:4.1f}s] Frame {frame_count}") + print("-" * 70) + + # ==================================================== + # Manually poll DeviceIO trackers for tracked objects + # ==================================================== + hand_left_tracked = hand_tracker.get_left_hand(session) + hand_right_tracked = hand_tracker.get_right_hand(session) + head_tracked = head_tracker.get_head(session) + left_ctrl_tracked = controller_tracker.get_left_controller( + session + ) + right_ctrl_tracked = controller_tracker.get_right_controller( + session + ) + + # ==================================================== + # Wrap tracked objects in TensorGroups for source inputs + # ==================================================== + + hands_inputs = {} + hands_input_spec = hands_source.input_spec() + for input_name, group_type in hands_input_spec.items(): + tg = TensorGroup(group_type) + if "left" in input_name.lower(): + tg[0] = hand_left_tracked + elif "right" in input_name.lower(): + tg[0] = hand_right_tracked + hands_inputs[input_name] = tg + + head_inputs = {} + head_input_spec = head_source.input_spec() + for input_name, group_type in head_input_spec.items(): + tg = TensorGroup(group_type) + tg[0] = head_tracked + head_inputs[input_name] = tg + + controllers_inputs = {} + controllers_input_spec = controllers_source.input_spec() + for input_name, group_type in controllers_input_spec.items(): + tg = TensorGroup(group_type) + if "left" in input_name.lower(): + tg[0] = left_ctrl_tracked + elif "right" in input_name.lower(): + tg[0] = right_ctrl_tracked + controllers_inputs[input_name] = tg + + # ==================================================== + # Pass wrapped data to combiner with correct structure + # ==================================================== + all_data = combiner( + { + "hands": hands_inputs, + "head": head_inputs, + "controllers": controllers_inputs, + } ) - print( - f" Right: {'ACTIVE' if not right_hand.is_none else 'INACTIVE'}" - ) - if not right_hand.is_none: - right_positions = right_hand[HandInputIndex.JOINT_POSITIONS] - wrist_idx = deviceio.JOINT_WRIST - wrist_pos = right_positions[wrist_idx] + # Extract hand data (now in tensor format) + # Source nodes emit None when tracking is inactive (Optional). + left_hand = all_data[HandsSource.LEFT] + right_hand = all_data[HandsSource.RIGHT] + + print(" Hands:") print( - f" Wrist: [{wrist_pos[0]:6.3f}, {wrist_pos[1]:6.3f}, {wrist_pos[2]:6.3f}]" + f" Left: {'ACTIVE' if not left_hand.is_none else 'INACTIVE'}" ) + if not left_hand.is_none: + left_positions = left_hand[HandInputIndex.JOINT_POSITIONS] + wrist_idx = deviceio.JOINT_WRIST + wrist_pos = left_positions[wrist_idx] + print( + f" Wrist: [{wrist_pos[0]:6.3f}, {wrist_pos[1]:6.3f}, {wrist_pos[2]:6.3f}]" + ) - # Extract head data (Optional — absent when no tracker) - head = all_data["head"] - - print(" Head:") - if head.is_none: - print(" Status: ABSENT (no tracker)") - else: - head_valid = head[HeadPoseIndex.IS_VALID] - print(f" Status: {'VALID' if head_valid else 'INVALID'}") - if head_valid: - head_position = head[HeadPoseIndex.POSITION] + print( + f" Right: {'ACTIVE' if not right_hand.is_none else 'INACTIVE'}" + ) + if not right_hand.is_none: + right_positions = right_hand[HandInputIndex.JOINT_POSITIONS] + wrist_idx = deviceio.JOINT_WRIST + wrist_pos = right_positions[wrist_idx] print( - f" Position: [{head_position[0]:6.3f}, {head_position[1]:6.3f}, {head_position[2]:6.3f}]" + f" Wrist: [{wrist_pos[0]:6.3f}, {wrist_pos[1]:6.3f}, {wrist_pos[2]:6.3f}]" ) - # Extract controller data - left_controller = all_data[ControllersSource.LEFT] - right_controller = all_data[ControllersSource.RIGHT] - - print(" Controllers:") - print( - f" Left: {'ACTIVE' if not left_controller.is_none else 'INACTIVE'}" - ) - if not left_controller.is_none: - left_trigger = left_controller[ - ControllerInputIndex.TRIGGER_VALUE - ] - print(f" Trigger: {left_trigger:4.2f}") - - print( - f" Right: {'ACTIVE' if not right_controller.is_none else 'INACTIVE'}" - ) - if not right_controller.is_none: - right_trigger = right_controller[ - ControllerInputIndex.TRIGGER_VALUE - ] - print(f" Trigger: {right_trigger:4.2f}") - - print() - - frame_count += 1 - time.sleep(0.016) # ~60 FPS - - # ================================================================ - # Cleanup - # ================================================================ - print() - print("=" * 70) - print(f"Processed {frame_count} frames ({frame_count / 10.0:.1f} FPS)") - print("=" * 70) - print("\nCleaning up...") - print(" ✓ Resources will be cleaned up automatically (RAII)") + # Extract head data (Optional — absent when no tracker) + head = all_data["head"] + + print(" Head:") + if head.is_none: + print(" Status: ABSENT (no tracker)") + else: + head_valid = head[HeadPoseIndex.IS_VALID] + print(f" Status: {'VALID' if head_valid else 'INVALID'}") + if head_valid: + head_position = head[HeadPoseIndex.POSITION] + print( + f" Position: [{head_position[0]:6.3f}, {head_position[1]:6.3f}, {head_position[2]:6.3f}]" + ) + + # Extract controller data + left_controller = all_data[ControllersSource.LEFT] + right_controller = all_data[ControllersSource.RIGHT] + + print(" Controllers:") + print( + f" Left: {'ACTIVE' if not left_controller.is_none else 'INACTIVE'}" + ) + if not left_controller.is_none: + left_trigger = left_controller[ + ControllerInputIndex.TRIGGER_VALUE + ] + print(f" Trigger: {left_trigger:4.2f}") + + print( + f" Right: {'ACTIVE' if not right_controller.is_none else 'INACTIVE'}" + ) + if not right_controller.is_none: + right_trigger = right_controller[ + ControllerInputIndex.TRIGGER_VALUE + ] + print(f" Trigger: {right_trigger:4.2f}") + + print() + + frame_count += 1 + time.sleep(0.016) # ~60 FPS + + # ================================================================ + # Cleanup + # ================================================================ + print() + print("=" * 70) + print(f"Processed {frame_count} frames ({frame_count / 10.0:.1f} FPS)") + print("=" * 70) + print("\nCleaning up...") + print(" ✓ Resources will be cleaned up automatically (RAII)") print("\n✅ Example completed successfully!") return 0 diff --git a/examples/teleop/python/dex_bimanual_example.py b/examples/teleop/python/dex_bimanual_example.py index 6ec1a5598..134f8fbba 100644 --- a/examples/teleop/python/dex_bimanual_example.py +++ b/examples/teleop/python/dex_bimanual_example.py @@ -14,6 +14,7 @@ from types import SimpleNamespace from pathlib import Path +from isaacteleop.cloudxr import CloudXRLauncher from isaacteleop.retargeting_engine.deviceio_source_nodes import HandsSource from isaacteleop.retargeters import ( DexHandRetargeter, @@ -51,6 +52,7 @@ def main(): parser.add_argument( "--enable-tuning", action="store_true", help="Enable retargeting tuning UI" ) + CloudXRLauncher.add_launcher_arguments(parser) args = parser.parse_args() # Check for config files @@ -151,45 +153,48 @@ def main(): # Create and run TeleopSession # ================================================================== - session_config = TeleopSessionConfig( - app_name="DexBiManualExample", - trackers=[], # Auto-discovered from pipeline - pipeline=pipeline, - ) - - # Access the internal retargeters for tuning - retargeters_to_tune = [left_retargeter, right_retargeter] - - # Open the UI using the context manager - if args.enable_tuning: - print("Opening Retargeting UI...") - ui_context = MultiRetargeterTuningUIImGui( - retargeters_to_tune, title="Hand Retargeting Tuning" + with CloudXRLauncher.launch_context(args): + session_config = TeleopSessionConfig( + app_name="DexBiManualExample", + trackers=[], # Auto-discovered from pipeline + pipeline=pipeline, ) - else: - ui_context = contextlib.nullcontext(SimpleNamespace(is_running=lambda: True)) - - with ui_context as ui: - with TeleopSession(session_config) as session: - start_time = time.time() - - while time.time() - start_time < 360.0 and ui.is_running(): - result = session.step() - - # Output: Combined joint angles - # result["left_hand_joints"] and result["right_hand_joints"] are TensorGroups - left_vals = list(result["left_hand_joints"]) - right_vals = list(result["right_hand_joints"]) - - if session.frame_count % 30 == 0: - elapsed = session.get_elapsed_time() - # Print first few joints from left and right parts - l_print = left_vals[: min(3, len(left_vals))] - r_print = right_vals[: min(3, len(right_vals))] - - print(f"[{elapsed:5.1f}s] L: {l_print} ... R: {r_print} ...") - time.sleep(0.016) + # Access the internal retargeters for tuning + retargeters_to_tune = [left_retargeter, right_retargeter] + + # Open the UI using the context manager + if args.enable_tuning: + print("Opening Retargeting UI...") + ui_context = MultiRetargeterTuningUIImGui( + retargeters_to_tune, title="Hand Retargeting Tuning" + ) + else: + ui_context = contextlib.nullcontext( + SimpleNamespace(is_running=lambda: True) + ) + + with ui_context as ui: + with TeleopSession(session_config) as session: + start_time = time.time() + + while time.time() - start_time < 360.0 and ui.is_running(): + result = session.step() + + # Output: Combined joint angles + # result["left_hand_joints"] and result["right_hand_joints"] are TensorGroups + left_vals = list(result["left_hand_joints"]) + right_vals = list(result["right_hand_joints"]) + + if session.frame_count % 30 == 0: + elapsed = session.get_elapsed_time() + # Print first few joints from left and right parts + l_print = left_vals[: min(3, len(left_vals))] + r_print = right_vals[: min(3, len(right_vals))] + + print(f"[{elapsed:5.1f}s] L: {l_print} ... R: {r_print} ...") + + time.sleep(0.016) return 0 diff --git a/examples/teleop/python/full_bimanual_reordering_example.py b/examples/teleop/python/full_bimanual_reordering_example.py index bcf84cb80..39ed74731 100644 --- a/examples/teleop/python/full_bimanual_reordering_example.py +++ b/examples/teleop/python/full_bimanual_reordering_example.py @@ -18,6 +18,7 @@ from pathlib import Path import numpy as np +from isaacteleop.cloudxr import CloudXRLauncher from isaacteleop.retargeting_engine.deviceio_source_nodes import HandsSource from isaacteleop.retargeters import ( DexHandRetargeter, @@ -51,6 +52,7 @@ def main(): parser.add_argument( "--enable-tuning", action="store_true", help="Enable retargeting tuning UI" ) + CloudXRLauncher.add_launcher_arguments(parser) args = parser.parse_args() # Config paths (similar to dex_bimanual_example) @@ -204,66 +206,71 @@ def main(): # 3. Run Session # ================================================================== - session_config = TeleopSessionConfig( - app_name="FullBimanualExample", - trackers=[], - pipeline=pipeline, - ) - - # UI setup - retargeters_to_tune = [ - left_hand_retargeter, - right_hand_retargeter, - left_arm_retargeter, - right_arm_retargeter, - ] - - if args.enable_tuning: - print("Opening Retargeting UI...") - ui_context = MultiRetargeterTuningUIImGui( - retargeters_to_tune, title="Full Bimanual Tuning" + with CloudXRLauncher.launch_context(args): + session_config = TeleopSessionConfig( + app_name="FullBimanualExample", + trackers=[], + pipeline=pipeline, ) - else: - ui_context = contextlib.nullcontext(SimpleNamespace(is_running=lambda: True)) - - with ui_context as ui: - with TeleopSession(session_config) as session: - start_time = time.time() - - print("\nStarting Loop. Press Ctrl+C to exit (or close UI window).") - print("Outputting flattened 'action' tensor...") - - while time.time() - start_time < 360.0 and ui.is_running(): - result = session.step() - - # result["action"] is a TensorGroup containing ONE tensor (our array) - # Access it at index 0 - action_tensor = result["action"][0] # This is a numpy array (float32) - - if session.frame_count % 60 == 0: - elapsed = session.get_elapsed_time() - # Print summary of the tensor - # e.g. Left Arm Pos - l_pos = action_tensor[0:3] - # Right Arm Pos (index depends on length of left hand joints) - r_start_idx = 7 + len(left_hand_retargeter._hand_joint_names) - r_pos = action_tensor[r_start_idx : r_start_idx + 3] - - # Left Hand Joints (first 3) - l_hand_start = 7 - l_hand_joints = action_tensor[l_hand_start : l_hand_start + 3] - - # Right Hand Joints (first 3) - r_hand_start = r_start_idx + 7 - r_hand_joints = action_tensor[r_hand_start : r_hand_start + 3] - - print( - f"[{elapsed:5.1f}s] Action Shape: {action_tensor.shape} | " - f"L_Arm: {np.round(l_pos, 3)} | L_Hand(3): {np.round(l_hand_joints, 3)} | " - f"R_Arm: {np.round(r_pos, 3)} | R_Hand(3): {np.round(r_hand_joints, 3)}" - ) - - time.sleep(0.016) + + # UI setup + retargeters_to_tune = [ + left_hand_retargeter, + right_hand_retargeter, + left_arm_retargeter, + right_arm_retargeter, + ] + + if args.enable_tuning: + print("Opening Retargeting UI...") + ui_context = MultiRetargeterTuningUIImGui( + retargeters_to_tune, title="Full Bimanual Tuning" + ) + else: + ui_context = contextlib.nullcontext( + SimpleNamespace(is_running=lambda: True) + ) + + with ui_context as ui: + with TeleopSession(session_config) as session: + start_time = time.time() + + print("\nStarting Loop. Press Ctrl+C to exit (or close UI window).") + print("Outputting flattened 'action' tensor...") + + while time.time() - start_time < 360.0 and ui.is_running(): + result = session.step() + + # result["action"] is a TensorGroup containing ONE tensor (our array) + # Access it at index 0 + action_tensor = result["action"][ + 0 + ] # This is a numpy array (float32) + + if session.frame_count % 60 == 0: + elapsed = session.get_elapsed_time() + # Print summary of the tensor + # e.g. Left Arm Pos + l_pos = action_tensor[0:3] + # Right Arm Pos (index depends on length of left hand joints) + r_start_idx = 7 + len(left_hand_retargeter._hand_joint_names) + r_pos = action_tensor[r_start_idx : r_start_idx + 3] + + # Left Hand Joints (first 3) + l_hand_start = 7 + l_hand_joints = action_tensor[l_hand_start : l_hand_start + 3] + + # Right Hand Joints (first 3) + r_hand_start = r_start_idx + 7 + r_hand_joints = action_tensor[r_hand_start : r_hand_start + 3] + + print( + f"[{elapsed:5.1f}s] Action Shape: {action_tensor.shape} | " + f"L_Arm: {np.round(l_pos, 3)} | L_Hand(3): {np.round(l_hand_joints, 3)} | " + f"R_Arm: {np.round(r_pos, 3)} | R_Hand(3): {np.round(r_hand_joints, 3)}" + ) + + time.sleep(0.016) return 0 diff --git a/examples/teleop/python/isaac_lab_gripper_example.py b/examples/teleop/python/isaac_lab_gripper_example.py index 40f33c671..4d57cc1e3 100644 --- a/examples/teleop/python/isaac_lab_gripper_example.py +++ b/examples/teleop/python/isaac_lab_gripper_example.py @@ -12,6 +12,7 @@ import time import isaacteleop.deviceio as deviceio +from isaacteleop.cloudxr import CloudXRLauncher from isaacteleop.retargeters import ( GripperRetargeter, GripperRetargeterConfig, @@ -72,26 +73,27 @@ def main(): pipeline=pipeline, ) - with TeleopSession(session_config) as session: - # No session injection needed + with CloudXRLauncher(): + with TeleopSession(session_config) as session: + # No session injection needed - start_time = time.time() + start_time = time.time() - while time.time() - start_time < 30.0: - result = session.step() + while time.time() - start_time < 30.0: + result = session.step() - # Output: -1.0 (closed) or 1.0 (open) - cmd = result["gripper_command"][0] - state = "CLOSED" if cmd < 0 else "OPEN" + # Output: -1.0 (closed) or 1.0 (open) + cmd = result["gripper_command"][0] + state = "CLOSED" if cmd < 0 else "OPEN" - # Print status every 0.2 seconds - if session.frame_count % 12 == 0: - elapsed = session.get_elapsed_time() - print(f"[{elapsed:5.1f}s] Gripper Command: {cmd:.1f} ({state})") + # Print status every 0.2 seconds + if session.frame_count % 12 == 0: + elapsed = session.get_elapsed_time() + print(f"[{elapsed:5.1f}s] Gripper Command: {cmd:.1f} ({state})") - time.sleep(0.016) + time.sleep(0.016) - print("\nTime limit reached.") + print("\nTime limit reached.") return 0 diff --git a/examples/teleop/python/joint_space_device_example.py b/examples/teleop/python/joint_space_device_example.py index 0e0febc93..7f17d3252 100644 --- a/examples/teleop/python/joint_space_device_example.py +++ b/examples/teleop/python/joint_space_device_example.py @@ -12,10 +12,11 @@ It consumes joint state streamed by the real ``so101_leader`` plugin over the OpenXR tensor transport via a ``TeleopSession`` (the ``JointStateSource`` auto-discovers and polls the -``JointStateTracker``). Like the other CloudXR examples/tests, it **expects the CloudXR runtime -to already be running** -- ``source ~/.cloudxr/run/cloudxr.env`` first -- and does not probe for -it. Use ``--launch-plugin`` to spawn the synthetic plugin process automatically; otherwise start -``so101_leader`` (or any device pushing the same ``collection_id``) separately. +``JointStateTracker``). By default ``CloudXRLauncher`` starts the CloudXR runtime and WSS +proxy in-process; pass ``--no-launch-cloudxr-runtime`` if you already sourced +``~/.cloudxr/run/cloudxr.env``. Use ``--launch-plugin`` to spawn the synthetic plugin +process automatically; otherwise start ``so101_leader`` (or any device pushing the same +``collection_id``) separately. Two modes: @@ -25,7 +26,6 @@ Examples:: - source ~/.cloudxr/run/cloudxr.env python joint_space_device_example.py --launch-plugin --mode joint --frames 8 python joint_space_device_example.py --launch-plugin --mode ee --urdf /path/to/so101_new_calib.urdf """ @@ -39,6 +39,7 @@ import numpy as np +from isaacteleop.cloudxr import CloudXRLauncher from isaacteleop.retargeting_engine.deviceio_source_nodes import JointStateSource from isaacteleop.retargeting_engine.interface import OutputCombiner from isaacteleop.retargeters import ( @@ -166,7 +167,7 @@ def run_live( if len(actions) < num_frames: raise SystemExit( f"FAILED: only {len(actions)}/{num_frames} action(s) received from the live plugin " - f"within {timeout_s:.0f}s (is the so101_leader plugin pushing? is cloudxr.env sourced?)" + f"within {timeout_s:.0f}s (is the so101_leader plugin pushing?)" ) # A single received frame can't be "stale" -- only flag multi-frame runs that never change. varied = len(actions) <= 1 or any( @@ -210,6 +211,7 @@ def main() -> None: default=20.0, help="Seconds to wait for plugin frames", ) + CloudXRLauncher.add_launcher_arguments(parser) args = parser.parse_args() plugin_proc = None @@ -223,7 +225,8 @@ def main() -> None: plugin_proc = subprocess.Popen([args.plugin_bin, "", _COLLECTION_ID]) time.sleep(1.5) # let it create its OpenXR session and start pushing try: - run_live(args.mode, args.frames, args.urdf, args.ee_link, args.timeout) + with CloudXRLauncher.launch_context(args): + run_live(args.mode, args.frames, args.urdf, args.ee_link, args.timeout) finally: if plugin_proc is not None: plugin_proc.terminate() diff --git a/examples/teleop/python/se3_retargeting_example.py b/examples/teleop/python/se3_retargeting_example.py index cd6ea4c17..0f8c01c86 100644 --- a/examples/teleop/python/se3_retargeting_example.py +++ b/examples/teleop/python/se3_retargeting_example.py @@ -12,6 +12,7 @@ import time import numpy as np +from isaacteleop.cloudxr import CloudXRLauncher from isaacteleop.retargeting_engine.deviceio_source_nodes import ( HandsSource, ControllersSource, @@ -141,27 +142,28 @@ def run_rel_example(use_controller=False): def main(): - print("=" * 80) - print(" SE3 Retargeting Examples") - print("=" * 80) - print("1. Absolute Positioning (Hand -> Pose)") - print("2. Absolute Positioning (Controller -> Pose)") - print("3. Relative Positioning (Hand Delta -> Delta)") - print("4. Relative Positioning (Controller Delta -> Delta)") - - choice = input("\nEnter choice (1-4): ").strip() - - if choice == "1": - run_abs_example(use_controller=False) - elif choice == "2": - run_abs_example(use_controller=True) - elif choice == "3": - run_rel_example(use_controller=False) - elif choice == "4": - run_rel_example(use_controller=True) - else: - print("Invalid choice") - return 1 + with CloudXRLauncher(): + print("=" * 80) + print(" SE3 Retargeting Examples") + print("=" * 80) + print("1. Absolute Positioning (Hand -> Pose)") + print("2. Absolute Positioning (Controller -> Pose)") + print("3. Relative Positioning (Hand Delta -> Delta)") + print("4. Relative Positioning (Controller Delta -> Delta)") + + choice = input("\nEnter choice (1-4): ").strip() + + if choice == "1": + run_abs_example(use_controller=False) + elif choice == "2": + run_abs_example(use_controller=True) + elif choice == "3": + run_rel_example(use_controller=False) + elif choice == "4": + run_rel_example(use_controller=True) + else: + print("Invalid choice") + return 1 return 0 diff --git a/examples/teleop_session_manager/python/message_channel_example.py b/examples/teleop_session_manager/python/message_channel_example.py index 892b860f8..e5c607089 100755 --- a/examples/teleop_session_manager/python/message_channel_example.py +++ b/examples/teleop_session_manager/python/message_channel_example.py @@ -15,6 +15,7 @@ import time import uuid +from isaacteleop.cloudxr import CloudXRLauncher from isaacteleop.retargeting_engine.deviceio_source_nodes import ( MessageChannelConnectionStatus, message_channel_config, @@ -75,6 +76,7 @@ def main() -> int: default=256, help="Bounded outbound queue length", ) + CloudXRLauncher.add_launcher_arguments(parser) args = parser.parse_args() source, sink = message_channel_config( @@ -100,35 +102,36 @@ def main() -> int: send_counter = 0 last_send_time = 0.0 - with TeleopSession(config) as session: - while True: - result = session.step() - status = result["status"][0] - messages_tracked = result["messages_tracked"][0] - messages = ( - messages_tracked.data if messages_tracked.data is not None else [] - ) - - for msg in messages: - payload = bytes(msg.payload) - try: - decoded = payload.decode("utf-8") - print(f"[rx] {decoded}") - except UnicodeDecodeError: - print(f"[rx] 0x{payload.hex()}") - - now = time.monotonic() - if ( - status == MessageChannelConnectionStatus.CONNECTED - and now - last_send_time >= 1.0 - ): - payload_text = f"hello #{send_counter} @ {time.time():.3f}" - _enqueue_outbound_message(sink, payload_text.encode("utf-8")) - print(f"[tx] {payload_text}") - last_send_time = now - send_counter += 1 - - time.sleep(0.01) + with CloudXRLauncher.launch_context(args): + with TeleopSession(config) as session: + while True: + result = session.step() + status = result["status"][0] + messages_tracked = result["messages_tracked"][0] + messages = ( + messages_tracked.data if messages_tracked.data is not None else [] + ) + + for msg in messages: + payload = bytes(msg.payload) + try: + decoded = payload.decode("utf-8") + print(f"[rx] {decoded}") + except UnicodeDecodeError: + print(f"[rx] 0x{payload.hex()}") + + now = time.monotonic() + if ( + status == MessageChannelConnectionStatus.CONNECTED + and now - last_send_time >= 1.0 + ): + payload_text = f"hello #{send_counter} @ {time.time():.3f}" + _enqueue_outbound_message(sink, payload_text.encode("utf-8")) + print(f"[tx] {payload_text}") + last_send_time = now + send_counter += 1 + + time.sleep(0.01) return 0 diff --git a/examples/teleop_session_manager/python/teleop_controls_simple_example.py b/examples/teleop_session_manager/python/teleop_controls_simple_example.py index f9317f03f..750729da2 100644 --- a/examples/teleop_session_manager/python/teleop_controls_simple_example.py +++ b/examples/teleop_session_manager/python/teleop_controls_simple_example.py @@ -19,6 +19,7 @@ import time from typing import Dict +from isaacteleop.cloudxr import CloudXRLauncher from isaacteleop.retargeting_engine.deviceio_source_nodes import ( HeadSource, HandsSource, @@ -69,52 +70,58 @@ def _build_control_signals( def main() -> int: - head = HeadSource(name="head") - hands = HandsSource(name="hands") - controllers = ControllersSource(name="controllers") - - main_pipeline = build_observation_pipeline(head, hands, controllers) - - control_signals = _build_control_signals(controllers) - teleop_manager = DefaultTeleopStateManager(name="teleop_manager") - teleop_control_pipeline = teleop_manager.connect( - { - teleop_manager.INPUT_KILL: control_signals["kill_signal"].output("value"), - teleop_manager.INPUT_RUN_TOGGLE: control_signals[ - "run_toggle_signal" - ].output("value"), - teleop_manager.INPUT_RESET: control_signals["reset_signal"].output("value"), - } - ) - - config = TeleopSessionConfig( - app_name="TeleopControlsSimpleExample", - pipeline=main_pipeline, - teleop_control_pipeline=teleop_control_pipeline, - ) - - with TeleopSession(config) as session: - print_header() - - # Example high-level hook: a caller can gate robot power/control using context. - robot_enabled: bool | None = None - - while True: - outputs = session.step() - context = session.last_context - if context is not None: - enabled_now = ( - context.execution_events.execution_state != ExecutionState.STOPPED - ) - if robot_enabled is None or enabled_now != robot_enabled: - robot_enabled = enabled_now - print(f"[high-level] robot_enabled={robot_enabled}") - if context.execution_events.reset: - print("[high-level] reset pulse received") - - if session.frame_count % 30 == 0: - print_frame(outputs, session.get_elapsed_time()) - time.sleep(0.016) + with CloudXRLauncher(): + head = HeadSource(name="head") + hands = HandsSource(name="hands") + controllers = ControllersSource(name="controllers") + + main_pipeline = build_observation_pipeline(head, hands, controllers) + + control_signals = _build_control_signals(controllers) + teleop_manager = DefaultTeleopStateManager(name="teleop_manager") + teleop_control_pipeline = teleop_manager.connect( + { + teleop_manager.INPUT_KILL: control_signals["kill_signal"].output( + "value" + ), + teleop_manager.INPUT_RUN_TOGGLE: control_signals[ + "run_toggle_signal" + ].output("value"), + teleop_manager.INPUT_RESET: control_signals["reset_signal"].output( + "value" + ), + } + ) + + config = TeleopSessionConfig( + app_name="TeleopControlsSimpleExample", + pipeline=main_pipeline, + teleop_control_pipeline=teleop_control_pipeline, + ) + + with TeleopSession(config) as session: + print_header() + + # Example high-level hook: a caller can gate robot power/control using context. + robot_enabled: bool | None = None + + while True: + outputs = session.step() + context = session.last_context + if context is not None: + enabled_now = ( + context.execution_events.execution_state + != ExecutionState.STOPPED + ) + if robot_enabled is None or enabled_now != robot_enabled: + robot_enabled = enabled_now + print(f"[high-level] robot_enabled={robot_enabled}") + if context.execution_events.reset: + print("[high-level] reset pulse received") + + if session.frame_count % 30 == 0: + print_frame(outputs, session.get_elapsed_time()) + time.sleep(0.016) return 0