Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/build-ubuntu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -718,7 +718,7 @@ jobs:
-v /tmp:/tmp \
-e PYTHONUNBUFFERED=1 \
--entrypoint bash teleop_ros2_ref:${{ env.ROS_DISTRO }} -c \
"source /usr/local/bin/teleop-env-setup && exec uv run --no-sync teleop_ros2_node.py --ros-args -p mode:=$mode -p hand_retargeter:=$hand_retargeter -p mcap_replay_path:=$REPLAY_MCAP" >/dev/null
"source /usr/local/bin/teleop-env-setup && exec uv run --no-sync python -m isaacteleop_examples.teleop_ros2 --ros-args -p mode:=$mode -p hand_retargeter:=$hand_retargeter -p mcap_replay_path:=$REPLAY_MCAP" >/dev/null

deadline=$((SECONDS + READINESS_TIMEOUT_SEC))
while [ "$SECONDS" -lt "$deadline" ]; do
Expand All @@ -744,7 +744,7 @@ jobs:
if docker exec \
-e PYTHONUNBUFFERED=1 \
"$RUN_NAME" bash -c \
"source /usr/local/bin/teleop-env-setup && exec uv run --no-sync python -m integration_tests.teleop_ros2_topic_verifier --mode $mode --hand-retargeter $hand_retargeter"; then
"source /usr/local/bin/teleop-env-setup && exec uv run --no-sync python -m isaacteleop_examples.teleop_ros2.integration_tests.teleop_ros2_topic_verifier --mode $mode --hand-retargeter $hand_retargeter"; then
verifier_rc=0
else
verifier_rc=$?
Expand Down Expand Up @@ -825,7 +825,7 @@ jobs:
-e PYTHONUNBUFFERED=1 \
-e ACCEPT_CLOUDXR_EULA=Y \
--entrypoint bash "$IMAGE" -c \
"source /usr/local/bin/teleop-env-setup && exec uv run --no-sync teleop_ros2_node.py --ros-args -p mode:=$MODE -p hand_retargeter:=$HAND_RETARGETER -p hand_tracking_plugin:=$HAND_TRACKING_PLUGIN -p cloudxr_accept_eula:=true" >/dev/null
"source /usr/local/bin/teleop-env-setup && exec uv run --no-sync python -m isaacteleop_examples.teleop_ros2 --ros-args -p mode:=$MODE -p hand_retargeter:=$HAND_RETARGETER -p hand_tracking_plugin:=$HAND_TRACKING_PLUGIN -p cloudxr_accept_eula:=true" >/dev/null

fail() {
echo "Error: $1"
Expand Down
2 changes: 1 addition & 1 deletion examples/teleop_ros2/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ core library. Consumed by Isaac ROS Teleop.
## Docker Validation

- This reference integration needs ROS 2 plus the built `isaacteleop` wheel, which are not present in the dev container. Run/validate it inside the `examples/teleop_ros2/Dockerfile` image (the same path CI's `test-teleop-ros2` job uses), not directly on the host.
- To exercise it without live XR hardware, replay an MCAP fixture: build the image, run the installed `teleop_ros2_mcap_generator` to write a fixture, then run `teleop_ros2_node.py` with `-p mode:=<mode> -p mcap_replay_path:=<file>` and check topics (e.g. via `integration_tests/teleop_ros2_topic_verifier.py`). Replay mode does not launch CloudXR, so no GPU/NGC runtime is required. Share the fixture across containers with `-v /tmp:/tmp` and `--network host` for ROS 2 discovery.
- To exercise it without live XR hardware, replay an MCAP fixture: build the image, run the installed `teleop_ros2_mcap_generator` to write a fixture, then run `python -m isaacteleop_examples.teleop_ros2` with `-p mode:=<mode> -p mcap_replay_path:=<file>` and check topics (e.g. via `python -m isaacteleop_examples.teleop_ros2.integration_tests.teleop_ros2_topic_verifier`). Replay mode does not launch CloudXR, so no GPU/NGC runtime is required. Share the fixture across containers with `-v /tmp:/tmp` and `--network host` for ROS 2 discovery.
- The image build disables some CI gates (`-DENABLE_CLANG_FORMAT_CHECK=OFF`, `-DBUILD_TESTING=OFF`), so a green Docker build does not mean C++ formatting or ctest pass. Validate those separately.
- When creating temporary Docker images for `examples/teleop_ros2` validation, remove them before finishing the task unless the user explicitly asks to keep them.

Expand Down
2 changes: 1 addition & 1 deletion examples/teleop_ros2/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ include(${CMAKE_SOURCE_DIR}/cmake/InstallPythonExample.cmake)

add_subdirectory(cpp/integration_tests)

install_python_example(DESTINATION examples/teleop_ros2/python)
install_python_example(DESTINATION examples/teleop_ros2)

install(FILES README.md Dockerfile
DESTINATION examples/teleop_ros2
Expand Down
5 changes: 3 additions & 2 deletions examples/teleop_ros2/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ EOF
FROM base AS runtime_base
ARG PYTHON_VERSION

WORKDIR /opt/isaacteleop/install/examples/teleop_ros2/python
WORKDIR /opt/isaacteleop/install/examples/teleop_ros2

# The node launches the CloudXR runtime in-process via CloudXRLauncher, so this
# image needs GPU access and the NVIDIA Vulkan/EGL ICDs. Run with --gpus all.
Expand Down Expand Up @@ -254,4 +254,5 @@ COPY tests/python/examples/teleop_ros2/ tests/
FROM runtime_base AS runtime

# Use --no-sync because the venv is already provisioned above.
ENTRYPOINT ["/usr/local/bin/teleop-entrypoint", "uv", "run", "--no-sync", "teleop_ros2_node.py"]
ENTRYPOINT ["/usr/local/bin/teleop-entrypoint", "uv", "run", "--no-sync", \
"python", "-m", "isaacteleop_examples.teleop_ros2"]
2 changes: 1 addition & 1 deletion examples/teleop_ros2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ Source-tree users can populate the same local asset directory from the repo root
python3 examples/teleop_ros2/scripts/fetch_sharpa_wave_urdfs.py
```

Robot assets are never downloaded by `teleop_ros2_node.py` at runtime.
Robot assets are never downloaded by the node at runtime.

### OpenXR hand input sources

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ Source-tree users can populate this directory from the repo root:
python3 examples/teleop_ros2/scripts/fetch_sharpa_wave_urdfs.py
```

These robot model assets are not fetched at runtime by `teleop_ros2_node.py`.
These robot model assets are not fetched at runtime by the node.
38 changes: 38 additions & 0 deletions examples/teleop_ros2/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
# The dist name mirrors the import path, so an installed example claims no bare
# top-level name in site-packages.
name = "isaacteleop-examples-teleop-ros2"
version = "0.1.0"
description = "Isaac Teleop ROS 2 reference publisher"
requires-python = ">=3.11,<3.14"
dependencies = [
"isaacteleop[cloudxr,grounding,retargeters,wuji]",
"msgpack",
"msgpack-numpy",
# The ROS node installs pink_ik and dexpilot together. Require nlopt>=2.8 so
# dex-retargeting resolves to 0.5.x, keeping the env on NumPy 2 / Pinocchio 3.
"nlopt>=2.8.0",
]

[project.optional-dependencies]
dev = [
"pytest",
]

[tool.pytest.ini_options]
testpaths = ["../../tests/python/examples/teleop_ros2"]

# `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 `teleop_ros2/`.
[tool.hatch.build.targets.wheel]
only-include = ["python/isaacteleop_examples/teleop_ros2"]
sources = ["python"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

from .teleop_ros2_node import main

raise SystemExit(main())
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import msgpack
import rclpy
from constants import (
from ..constants import (
HAND_RETARGETERS,
LEFT_SHARPA_WAVE_JOINT_NAMES,
LEFT_WUJI_HAND_JOINT_NAMES,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,14 @@
HeadInputIndex,
)

from constants import BODY_JOINT_NAMES, HAND_POSE_JOINT_INDICES, HAND_POSE_NAMES
from geometry import (
from .constants import BODY_JOINT_NAMES, HAND_POSE_JOINT_INDICES, HAND_POSE_NAMES
from .geometry import (
apply_manus_controller_to_hand_pose,
apply_transform_to_pose,
make_transform,
to_pose,
)
from tensor_group_helpers import (
from .tensor_group_helpers import (
controller_aim_is_valid,
hand_wrist_is_valid,
head_is_valid,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from pathlib import Path

import numpy as np
from constants import (
from .constants import (
HAND_RETARGETERS,
HAND_TRACKING_PLUGINS,
TELEOP_MODES,
Expand Down Expand Up @@ -228,7 +228,9 @@ def _load_config_asset_root(node: Node) -> Path:
f"config_asset_root directory not found: {config_asset_root}"
)
else:
config_asset_root = Path(__file__).resolve().parents[1]
# Four levels up is the example root, where configs/ and assets/ sit --
# in the source tree and in the install tree alike.
config_asset_root = Path(__file__).resolve().parents[3]
node.get_logger().info(f"Config/asset root: {config_asset_root}")
return config_asset_root

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@

from collections.abc import Sequence

from assets import (
from .assets import (
resolve_dex_sharpa_config,
resolve_dex_sharpa_urdf,
resolve_sharpa_mjcf,
)
from constants import (
from .constants import (
DEX_HANDTRACKING_TO_BASELINK_FRAME_TRANSFORM,
LEFT_FINGER_JOINT_NAMES,
LEFT_SHARPA_WAVE_JOINT_NAMES,
Expand Down Expand Up @@ -49,12 +49,12 @@
SessionMode,
TeleopSessionConfig,
)
from node_parameters import NodeParameters
from teleop_ros2_retargeters import (
from .node_parameters import NodeParameters
from .teleop_ros2_retargeters import (
HandTrackingGateRetargeter,
JointNameAliasRetargeter,
)
from tensor_group_helpers import joint_names_from_group_type
from .tensor_group_helpers import joint_names_from_group_type


def _maybe_alias_hand_joints(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from dataclasses import dataclass
from typing import TypedDict, cast

from constants import (
from .constants import (
SHARPA_HAND_RETARGETERS,
HandRetargeter,
HandTrackingPlugin,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
from isaacteleop.cloudxr import CloudXRLauncher
from isaacteleop.cloudxr.oob_teleop_env import TELEOP_CLIENT_ROUTE_ENV
from isaacteleop.teleop_session_manager import SessionMode, TeleopSession
from messages import (
from .messages import (
build_controller_msg,
build_ee_output_from_controllers,
build_ee_output_from_hands,
Expand All @@ -65,17 +65,17 @@
build_head_output,
build_root_command_output,
)
from teleop_profiles import (
from .teleop_profiles import (
PublishType,
SessionResult,
resolve_teleop_profile_spec,
validate_session_result,
)
from node_parameters import (
from .node_parameters import (
NodeParameters,
create_node_parameters,
)
from session_config import build_session_config
from .session_config import build_session_config


class TeleopRos2Node(Node):
Expand Down
25 changes: 0 additions & 25 deletions examples/teleop_ros2/python/pyproject.toml

This file was deleted.

4 changes: 2 additions & 2 deletions tests/python/examples/teleop_ros2/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
#
# teleop_ros2 unit tests. Each test_*.py registers as its own ctest entry under
# the ``teleop_ros2`` label. BUILD_EXAMPLE_TELEOP_ROS2 supplies the required ROS
# environment; the example source directory supplies its importable modules.
# environment; conftest.py puts the example's namespace root on sys.path.

file(GLOB TEST_FILES
RELATIVE "${CMAKE_CURRENT_SOURCE_DIR}"
Expand All @@ -20,7 +20,7 @@ foreach(test_file ${TEST_FILES})
)
set_tests_properties("teleop_ros2_${test_name}" PROPERTIES
ENVIRONMENT
"PYTHONPATH=${CMAKE_BINARY_DIR}/python_package/$<CONFIG>:${CMAKE_SOURCE_DIR}/examples/teleop_ros2/python"
"PYTHONPATH=${CMAKE_BINARY_DIR}/python_package/$<CONFIG>"
LABELS "teleop_ros2"
)
endforeach()
27 changes: 27 additions & 0 deletions tests/python/examples/teleop_ros2/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

# Resolve `isaacteleop_examples.teleop_ros2` against the in-tree source, so a
# bare `pytest` works and the ctest ENVIRONMENT needs no example path.
#
# These files are also copied into the teleop_ros2 container
# (Dockerfile: `COPY tests/python/examples/teleop_ros2/ tests/`), where
# tests/python/repo_paths.py does not exist and the example is already installed
# into the venv by `uv sync`. So the source-tree wiring is conditional: without
# it, importing repo_paths raises and every test errors during collection.

import sys
from pathlib import Path

_tests_python = Path(__file__).resolve().parents[2]

if (_tests_python / "repo_paths.py").is_file():
if str(_tests_python) not in sys.path:
sys.path.insert(0, str(_tests_python))

from repo_paths import repo_root # noqa: E402

# python/, not python/isaacteleop_examples/: that is a PEP 420 namespace. Do
# not add an __init__.py to make an import work -- it breaks the installed
# wheel's ability to share the namespace.
sys.path.insert(0, str(repo_root() / "examples" / "teleop_ros2" / "python"))
2 changes: 1 addition & 1 deletion tests/python/examples/teleop_ros2/test_geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import pytest
from scipy.spatial.transform import Rotation

from geometry import (
from isaacteleop_examples.teleop_ros2.geometry import (
apply_manus_controller_to_hand_pose,
apply_transform_to_pose,
to_pose,
Expand Down
4 changes: 2 additions & 2 deletions tests/python/examples/teleop_ros2/test_messages.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@
RobotHandJoints,
)

from constants import BODY_JOINT_NAMES, HAND_POSE_NAMES
from messages import (
from isaacteleop_examples.teleop_ros2.constants import BODY_JOINT_NAMES, HAND_POSE_NAMES
from isaacteleop_examples.teleop_ros2.messages import (
build_controller_msg,
build_ee_output_from_controllers,
build_ee_output_from_hands,
Expand Down
6 changes: 3 additions & 3 deletions tests/python/examples/teleop_ros2/test_teleop_profiles.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
from types import SimpleNamespace

import pytest
import session_config
from constants import (
from isaacteleop_examples.teleop_ros2 import session_config
from isaacteleop_examples.teleop_ros2.constants import (
LEFT_WUJI_HAND_JOINT_NAMES,
RIGHT_WUJI_HAND_JOINT_NAMES,
TELEOP_MODES,
Expand All @@ -18,7 +18,7 @@
TeleopMode,
resolve_hand_retargeter,
)
from teleop_profiles import (
from isaacteleop_examples.teleop_ros2.teleop_profiles import (
TELEOP_PROFILE_SPECS,
PublishType,
TeleopProfile,
Expand Down
Loading