From 99c31462b1c7895cb2f275b81862117e3c6a32eb Mon Sep 17 00:00:00 2001 From: "lei.hu" Date: Tue, 30 Jun 2026 18:46:06 +0800 Subject: [PATCH] Add G1-Wuji teleop example Signed-off-by: lei.hu --- CMakeLists.txt | 1 + examples/g1_wuji_teleop/CMakeLists.txt | 12 + examples/g1_wuji_teleop/README.md | 45 + .../g1_wuji_teleop/assets/g1_wuji/g1_wuji.usd | 3 + .../wuji_hand/meshes/left/finger1_link1.STL | 3 + .../meshes/left/finger1_link1_collision.STL | 3 + .../wuji_hand/meshes/left/finger1_link2.STL | 3 + .../meshes/left/finger1_link2_collision.STL | 3 + .../wuji_hand/meshes/left/finger1_link3.STL | 3 + .../meshes/left/finger1_link3_collision.STL | 3 + .../wuji_hand/meshes/left/finger1_link4.STL | 3 + .../meshes/left/finger1_link4_collision.STL | 3 + .../meshes/left/finger1_tip_link.STL | 3 + .../left/finger1_tip_link_collision.STL | 3 + .../wuji_hand/meshes/left/finger2_link1.STL | 3 + .../meshes/left/finger2_link1_collision.STL | 3 + .../wuji_hand/meshes/left/finger2_link2.STL | 3 + .../meshes/left/finger2_link2_collision.STL | 3 + .../wuji_hand/meshes/left/finger2_link3.STL | 3 + .../meshes/left/finger2_link3_collision.STL | 3 + .../wuji_hand/meshes/left/finger2_link4.STL | 3 + .../meshes/left/finger2_link4_collision.STL | 3 + .../meshes/left/finger2_tip_link.STL | 3 + .../left/finger2_tip_link_collision.STL | 3 + .../wuji_hand/meshes/left/finger3_link1.STL | 3 + .../meshes/left/finger3_link1_collision.STL | 3 + .../wuji_hand/meshes/left/finger3_link2.STL | 3 + .../meshes/left/finger3_link2_collision.STL | 3 + .../wuji_hand/meshes/left/finger3_link3.STL | 3 + .../meshes/left/finger3_link3_collision.STL | 3 + .../wuji_hand/meshes/left/finger3_link4.STL | 3 + .../meshes/left/finger3_link4_collision.STL | 3 + .../meshes/left/finger3_tip_link.STL | 3 + .../left/finger3_tip_link_collision.STL | 3 + .../wuji_hand/meshes/left/finger4_link1.STL | 3 + .../meshes/left/finger4_link1_collision.STL | 3 + .../wuji_hand/meshes/left/finger4_link2.STL | 3 + .../meshes/left/finger4_link2_collision.STL | 3 + .../wuji_hand/meshes/left/finger4_link3.STL | 3 + .../meshes/left/finger4_link3_collision.STL | 3 + .../wuji_hand/meshes/left/finger4_link4.STL | 3 + .../meshes/left/finger4_link4_collision.STL | 3 + .../meshes/left/finger4_tip_link.STL | 3 + .../left/finger4_tip_link_collision.STL | 3 + .../wuji_hand/meshes/left/finger5_link1.STL | 3 + .../meshes/left/finger5_link1_collision.STL | 3 + .../wuji_hand/meshes/left/finger5_link2.STL | 3 + .../meshes/left/finger5_link2_collision.STL | 3 + .../wuji_hand/meshes/left/finger5_link3.STL | 3 + .../meshes/left/finger5_link3_collision.STL | 3 + .../wuji_hand/meshes/left/finger5_link4.STL | 3 + .../meshes/left/finger5_link4_collision.STL | 3 + .../meshes/left/finger5_tip_link.STL | 3 + .../left/finger5_tip_link_collision.STL | 3 + .../wuji_hand/meshes/left/palm_link.STL | 3 + .../meshes/left/palm_link_collision.STL | 3 + .../wuji_hand/meshes/right/finger1_link1.STL | 3 + .../meshes/right/finger1_link1_collision.STL | 3 + .../wuji_hand/meshes/right/finger1_link2.STL | 3 + .../meshes/right/finger1_link2_collision.STL | 3 + .../wuji_hand/meshes/right/finger1_link3.STL | 3 + .../meshes/right/finger1_link3_collision.STL | 3 + .../wuji_hand/meshes/right/finger1_link4.STL | 3 + .../meshes/right/finger1_link4_collision.STL | 3 + .../meshes/right/finger1_tip_link.STL | 3 + .../right/finger1_tip_link_collision.STL | 3 + .../wuji_hand/meshes/right/finger2_link1.STL | 3 + .../meshes/right/finger2_link1_collision.STL | 3 + .../wuji_hand/meshes/right/finger2_link2.STL | 3 + .../meshes/right/finger2_link2_collision.STL | 3 + .../wuji_hand/meshes/right/finger2_link3.STL | 3 + .../meshes/right/finger2_link3_collision.STL | 3 + .../wuji_hand/meshes/right/finger2_link4.STL | 3 + .../meshes/right/finger2_link4_collision.STL | 3 + .../meshes/right/finger2_tip_link.STL | 3 + .../right/finger2_tip_link_collision.STL | 3 + .../wuji_hand/meshes/right/finger3_link1.STL | 3 + .../meshes/right/finger3_link1_collision.STL | 3 + .../wuji_hand/meshes/right/finger3_link2.STL | 3 + .../meshes/right/finger3_link2_collision.STL | 3 + .../wuji_hand/meshes/right/finger3_link3.STL | 3 + .../meshes/right/finger3_link3_collision.STL | 3 + .../wuji_hand/meshes/right/finger3_link4.STL | 3 + .../meshes/right/finger3_link4_collision.STL | 3 + .../meshes/right/finger3_tip_link.STL | 3 + .../right/finger3_tip_link_collision.STL | 3 + .../wuji_hand/meshes/right/finger4_link1.STL | 3 + .../meshes/right/finger4_link1_collision.STL | 3 + .../wuji_hand/meshes/right/finger4_link2.STL | 3 + .../meshes/right/finger4_link2_collision.STL | 3 + .../wuji_hand/meshes/right/finger4_link3.STL | 3 + .../meshes/right/finger4_link3_collision.STL | 3 + .../wuji_hand/meshes/right/finger4_link4.STL | 3 + .../meshes/right/finger4_link4_collision.STL | 3 + .../meshes/right/finger4_tip_link.STL | 3 + .../right/finger4_tip_link_collision.STL | 3 + .../wuji_hand/meshes/right/finger5_link1.STL | 3 + .../meshes/right/finger5_link1_collision.STL | 3 + .../wuji_hand/meshes/right/finger5_link2.STL | 3 + .../meshes/right/finger5_link2_collision.STL | 3 + .../wuji_hand/meshes/right/finger5_link3.STL | 3 + .../meshes/right/finger5_link3_collision.STL | 3 + .../wuji_hand/meshes/right/finger5_link4.STL | 3 + .../meshes/right/finger5_link4_collision.STL | 3 + .../meshes/right/finger5_tip_link.STL | 3 + .../right/finger5_tip_link_collision.STL | 3 + .../wuji_hand/meshes/right/palm_link.STL | 3 + .../meshes/right/palm_link_collision.STL | 3 + .../assets/wuji_hand/urdf/left.urdf | 1472 ++++++++++++++++ .../assets/wuji_hand/urdf/right.urdf | 1472 ++++++++++++++++ examples/g1_wuji_teleop/config/avp_manus.yaml | 46 + examples/g1_wuji_teleop/config/vr_manus.yaml | 50 + .../config/wuji_retargeting/left.yaml | 39 + .../config/wuji_retargeting/right.yaml | 39 + examples/g1_wuji_teleop/docs/setup.md | 125 ++ .../python/g1_wuji_teleop/__init__.py | 8 + .../python/g1_wuji_teleop/__main__.py | 12 + .../python/g1_wuji_teleop/app.py | 1530 +++++++++++++++++ .../python/g1_wuji_teleop/cli.py | 14 + .../python/g1_wuji_teleop/config.py | 47 + .../python/g1_wuji_teleop/input_stream.py | 506 ++++++ .../python/g1_wuji_teleop/retargeting.py | 198 +++ .../python/g1_wuji_teleop/robot.py | 1232 +++++++++++++ .../python/g1_wuji_teleop/scene.py | 291 ++++ .../python/g1_wuji_teleop/transforms.py | 64 + examples/g1_wuji_teleop/python/pyproject.toml | 13 + 126 files changed, 7531 insertions(+) create mode 100644 examples/g1_wuji_teleop/CMakeLists.txt create mode 100644 examples/g1_wuji_teleop/README.md create mode 100644 examples/g1_wuji_teleop/assets/g1_wuji/g1_wuji.usd create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_link1.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_link1_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_link2.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_link2_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_link3.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_link3_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_link4.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_link4_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_tip_link.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_tip_link_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_link1.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_link1_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_link2.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_link2_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_link3.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_link3_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_link4.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_link4_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_tip_link.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_tip_link_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_link1.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_link1_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_link2.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_link2_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_link3.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_link3_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_link4.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_link4_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_tip_link.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_tip_link_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_link1.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_link1_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_link2.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_link2_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_link3.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_link3_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_link4.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_link4_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_tip_link.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_tip_link_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_link1.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_link1_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_link2.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_link2_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_link3.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_link3_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_link4.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_link4_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_tip_link.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_tip_link_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/palm_link.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/palm_link_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_link1.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_link1_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_link2.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_link2_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_link3.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_link3_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_link4.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_link4_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_tip_link.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_tip_link_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_link1.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_link1_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_link2.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_link2_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_link3.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_link3_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_link4.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_link4_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_tip_link.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_tip_link_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_link1.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_link1_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_link2.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_link2_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_link3.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_link3_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_link4.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_link4_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_tip_link.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_tip_link_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_link1.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_link1_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_link2.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_link2_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_link3.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_link3_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_link4.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_link4_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_tip_link.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_tip_link_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_link1.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_link1_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_link2.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_link2_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_link3.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_link3_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_link4.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_link4_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_tip_link.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_tip_link_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/palm_link.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/palm_link_collision.STL create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/urdf/left.urdf create mode 100644 examples/g1_wuji_teleop/assets/wuji_hand/urdf/right.urdf create mode 100644 examples/g1_wuji_teleop/config/avp_manus.yaml create mode 100644 examples/g1_wuji_teleop/config/vr_manus.yaml create mode 100644 examples/g1_wuji_teleop/config/wuji_retargeting/left.yaml create mode 100644 examples/g1_wuji_teleop/config/wuji_retargeting/right.yaml create mode 100644 examples/g1_wuji_teleop/docs/setup.md create mode 100644 examples/g1_wuji_teleop/python/g1_wuji_teleop/__init__.py create mode 100644 examples/g1_wuji_teleop/python/g1_wuji_teleop/__main__.py create mode 100644 examples/g1_wuji_teleop/python/g1_wuji_teleop/app.py create mode 100644 examples/g1_wuji_teleop/python/g1_wuji_teleop/cli.py create mode 100644 examples/g1_wuji_teleop/python/g1_wuji_teleop/config.py create mode 100644 examples/g1_wuji_teleop/python/g1_wuji_teleop/input_stream.py create mode 100644 examples/g1_wuji_teleop/python/g1_wuji_teleop/retargeting.py create mode 100644 examples/g1_wuji_teleop/python/g1_wuji_teleop/robot.py create mode 100644 examples/g1_wuji_teleop/python/g1_wuji_teleop/scene.py create mode 100644 examples/g1_wuji_teleop/python/g1_wuji_teleop/transforms.py create mode 100644 examples/g1_wuji_teleop/python/pyproject.toml diff --git a/CMakeLists.txt b/CMakeLists.txt index 81e99a0b9..b2402f69b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -150,6 +150,7 @@ if(BUILD_EXAMPLES) add_subdirectory(examples/native_openxr) add_subdirectory(examples/mcap_record_replay) add_subdirectory(examples/haptic_feedback) + add_subdirectory(examples/g1_wuji_teleop) if(BUILD_VIZ) add_subdirectory(examples/camera_viz/tests) endif() diff --git a/examples/g1_wuji_teleop/CMakeLists.txt b/examples/g1_wuji_teleop/CMakeLists.txt new file mode 100644 index 000000000..b51ba8fdc --- /dev/null +++ b/examples/g1_wuji_teleop/CMakeLists.txt @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +cmake_minimum_required(VERSION 3.20) +project(g1_wuji_teleop_example) + +include(${CMAKE_SOURCE_DIR}/cmake/InstallPythonExample.cmake) +install_python_example(DESTINATION examples/g1_wuji_teleop/python) + +install(DIRECTORY config docs assets + DESTINATION examples/g1_wuji_teleop +) diff --git a/examples/g1_wuji_teleop/README.md b/examples/g1_wuji_teleop/README.md new file mode 100644 index 000000000..cc862fb0e --- /dev/null +++ b/examples/g1_wuji_teleop/README.md @@ -0,0 +1,45 @@ + + +# G1-Wuji Teleop Example + +This example runs a G1-Wuji Isaac Lab scene from an OpenXR headset and MANUS +gloves. Arm end-effector poses come from either AVP hand tracking or VR/PICO +controllers; finger targets come from MANUS skeletons retargeted with the +official `wuji-retargeting` package. + +## Layout + +- `assets/`: G1-Wuji robot USD and Wuji hand assets used by the example. +- `config/avp_manus.yaml`: AVP hand-wrist arm control with MANUS fingers. +- `config/vr_manus.yaml`: VR/PICO controller arm control with MANUS fingers. +- `config/wuji_retargeting/`: Wuji retargeting YAML files for both hands. +- `docs/setup.md`: host setup and launch runbook. +- `python/g1_wuji_teleop/`: Python implementation package. + +## Run + +Install IsaacTeleop, install `wuji-retargeting` into the same Python +environment, start the CloudXR runtime, and source +`~/.cloudxr/run/cloudxr.env`. See [docs/setup.md](docs/setup.md) for the full +runbook. + +AVP + MANUS: + +```bash +PYTHONPATH=examples/g1_wuji_teleop/python python -m g1_wuji_teleop \ + --config examples/g1_wuji_teleop/config/avp_manus.yaml \ + --device cuda:0 \ + --viz kit +``` + +VR/PICO + MANUS: + +```bash +PYTHONPATH=examples/g1_wuji_teleop/python python -m g1_wuji_teleop \ + --config examples/g1_wuji_teleop/config/vr_manus.yaml \ + --device cuda:0 \ + --viz kit +``` diff --git a/examples/g1_wuji_teleop/assets/g1_wuji/g1_wuji.usd b/examples/g1_wuji_teleop/assets/g1_wuji/g1_wuji.usd new file mode 100644 index 000000000..f5a8770aa --- /dev/null +++ b/examples/g1_wuji_teleop/assets/g1_wuji/g1_wuji.usd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:acc0afc786907290fcb81f796ede6fa791bbae75c37c8168f8b8dbbc9c1e3d51 +size 47977976 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_link1.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_link1.STL new file mode 100644 index 000000000..cb3cd3a36 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_link1.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b17f48ce8974b30536376ef150abc9b8abdf2b6028af52757d3ce90c3f8174be +size 349784 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_link1_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_link1_collision.STL new file mode 100644 index 000000000..6fc5ae36a --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_link1_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:853c85e7149580c96d8f6b221c6132595cc4dc5fc33e73dfccfbfc240ca6b161 +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_link2.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_link2.STL new file mode 100644 index 000000000..e0d32c792 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_link2.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:53b16e2fc88fa152189750f7176b717cdc2b0ad8ade3e91d546355b196274205 +size 92484 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_link2_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_link2_collision.STL new file mode 100644 index 000000000..bb5f9faa5 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_link2_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:34150ded0f71fc59a028eb40d6a747618a3aa1d0c44823550a59ec1b74451b9f +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_link3.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_link3.STL new file mode 100644 index 000000000..6f75864c7 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_link3.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ebcbd6e485881a948b825c8f3c5d8d7120a5fea8d2590bf27664c4dc72940ba9 +size 87984 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_link3_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_link3_collision.STL new file mode 100644 index 000000000..23ffb3756 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_link3_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:adb4f921e1ff437296077699dc770bb3a256353d53bbf6c9aa09f3d52985b3b8 +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_link4.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_link4.STL new file mode 100644 index 000000000..bfbf1c079 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_link4.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0dcb58b73f8ec2a491f6044a742e8e3a0cb8a9c77c5a9a899d36537ccb9da698 +size 64584 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_link4_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_link4_collision.STL new file mode 100644 index 000000000..552a22129 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_link4_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:65ed57875ed25de2141a4a195bbaf555f510cfc44de4e99681c6957df18c8b0c +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_tip_link.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_tip_link.STL new file mode 100644 index 000000000..593f336c7 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_tip_link.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ac9ddb655262dfa0a339783141eb8f31387e6314d92d9a84a06e960cb64bc86 +size 96384 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_tip_link_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_tip_link_collision.STL new file mode 100644 index 000000000..78fd167ff --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_tip_link_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:23e9a2e6058f523437d6ad0689d54d45c04d002c1dfac85bc073e16f6e8780b6 +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_link1.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_link1.STL new file mode 100644 index 000000000..a655f0e33 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_link1.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cfe8003977ed8fe69e04633d1cbfd09714f4fd1f5b6947197da3019bb7051959 +size 56184 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_link1_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_link1_collision.STL new file mode 100644 index 000000000..e8d4f311b --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_link1_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:29d52cbef248abfc5ce8df43fcf3ffa900635127cced5bf859d9694bfd58fbc2 +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_link2.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_link2.STL new file mode 100644 index 000000000..96fc99cf6 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_link2.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1131e9a85112b3ba9bc792544d8a522bec93412293668626f3d52ad9f7ffe8b6 +size 51984 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_link2_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_link2_collision.STL new file mode 100644 index 000000000..1d8dab458 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_link2_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4ef6ff2efef32ccc8ff8e2eee28db7463ba9ae05b158d5666d4bd5ef16161ad0 +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_link3.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_link3.STL new file mode 100644 index 000000000..cfa21284b --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_link3.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b5181c09e54860d98f3be64a644efdb664d134006f6c48549ed112f8f63c62fa +size 91284 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_link3_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_link3_collision.STL new file mode 100644 index 000000000..c508a882f --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_link3_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7990ea209c1a0456bcbc65c3b4bbf831b966635ca57cb324175c6592886e94f6 +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_link4.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_link4.STL new file mode 100644 index 000000000..659612df2 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_link4.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c64e2fdc5c192f8dc327dacf60b35609304ad5cea3fc7358d3f780aac4e235a2 +size 54884 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_link4_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_link4_collision.STL new file mode 100644 index 000000000..c4a3c3578 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_link4_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce7659ecfc46967b08da7ab064cefca5c6223c7576f22c7dcaf4fd8219983ca7 +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_tip_link.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_tip_link.STL new file mode 100644 index 000000000..bb48f1adf --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_tip_link.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1e8be5a90e81c319302053c9fa27608c900d47a5b67797fc931449885b9f86af +size 112784 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_tip_link_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_tip_link_collision.STL new file mode 100644 index 000000000..91cdf5105 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_tip_link_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1fdf002df36490d481a10593d476eb9239f666339559ed5562cb5d5ffc93a41d +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_link1.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_link1.STL new file mode 100644 index 000000000..e885af052 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_link1.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2ed2537fdea1584af89e562e9fc115c8ac06310f1c67f074ddc5a1eb57cdda6b +size 56184 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_link1_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_link1_collision.STL new file mode 100644 index 000000000..b45d2c42c --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_link1_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d8027c721b5c0c69fe2f04db0533d000c46fef43dc5db8a9430a01ae8ad88476 +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_link2.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_link2.STL new file mode 100644 index 000000000..a34be67c5 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_link2.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e76e1b3adbcbcb5f28fec54492a44c24d6ab9dfc6d8636c1cc4dc31f974559e +size 51984 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_link2_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_link2_collision.STL new file mode 100644 index 000000000..30301d0aa --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_link2_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0302da39f9c148cffb16d680fe5399efa0a022462ab9bb3937e3146cdfe3d88e +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_link3.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_link3.STL new file mode 100644 index 000000000..6a128f65b --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_link3.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e78e7bec8901a499e664b99900b96acd4b8f78a547af06753b09b90cd51e949b +size 91284 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_link3_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_link3_collision.STL new file mode 100644 index 000000000..561e5e206 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_link3_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ea57bfc0ca89c9f80e80702f5a6b22e96e38858c41b407cb390cbe081cae534 +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_link4.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_link4.STL new file mode 100644 index 000000000..f7bbb9a93 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_link4.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b7a16806e231d51ff50380b3d4b25bac3f347e6f13918c7373d018bcd9da11dd +size 54884 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_link4_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_link4_collision.STL new file mode 100644 index 000000000..4bdfd294b --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_link4_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:281eff1ca8541cf81d932edd676a5561ceee65ac2955b3af4df3906ba71e0bd3 +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_tip_link.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_tip_link.STL new file mode 100644 index 000000000..4c08a6ffb --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_tip_link.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b918123bb7f6dc4d94b6b62ceb01b5331d938066f5ce40b8138cb2aa8c755117 +size 113484 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_tip_link_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_tip_link_collision.STL new file mode 100644 index 000000000..a2140b781 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_tip_link_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4b7c505a71cae1ea747f44c9a808f615cda8859ea8472370e67a64e42a600908 +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_link1.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_link1.STL new file mode 100644 index 000000000..fb3c5aadb --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_link1.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0664072a389f47449eba9636583c40a2f95386ab23feacc2962fcb9a3070cb34 +size 56184 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_link1_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_link1_collision.STL new file mode 100644 index 000000000..27467a46c --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_link1_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ce0f42d7c684b7036bc5aeba3bd53ea7611dc4ad00592a448acaa95c8590491b +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_link2.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_link2.STL new file mode 100644 index 000000000..8afad4fd1 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_link2.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:eefec26f19d27322ccd9b2b1b080431bb6d63f3a4e1ea0b480c7d07c09eab655 +size 51984 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_link2_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_link2_collision.STL new file mode 100644 index 000000000..22b8c4b10 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_link2_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bc8ccd945de619743a9e94c0fe34e56afc346758ab5ea4439c5531c71d0383ca +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_link3.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_link3.STL new file mode 100644 index 000000000..d6ff1953a --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_link3.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6d27de502b0e12cff871037a7bd206b8caaa808cb1fd580305fbacf269b50d08 +size 91284 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_link3_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_link3_collision.STL new file mode 100644 index 000000000..ab072f515 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_link3_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:922b21c1e98f21b20b525f5060e07e07b7aa9f3fef0472b24c3a4d3ace7cc78b +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_link4.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_link4.STL new file mode 100644 index 000000000..468685557 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_link4.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb629c2fe0957bb6136b62a4f67ccb25839e79ad916b67c000bb3d726d9d9c2b +size 54884 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_link4_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_link4_collision.STL new file mode 100644 index 000000000..c0c26378b --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_link4_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c45c282dd5e3db4b66ccc90e84529f3df698e924a2f5dca53297632cbb17bf85 +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_tip_link.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_tip_link.STL new file mode 100644 index 000000000..f11927429 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_tip_link.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7d6739ac876ec9b7c6c36bd1fdd068389474407339cf97dbb93881b2cdf0f2cc +size 112784 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_tip_link_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_tip_link_collision.STL new file mode 100644 index 000000000..ee09d3433 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_tip_link_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6593376f3a70d40346661f5e042903e41d3040c630bfc5a66c72f55739804567 +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_link1.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_link1.STL new file mode 100644 index 000000000..922515657 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_link1.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8ff604ba894a320331516f0227aed75ffdd47d9a6ec29a8e792b2e7777bbb681 +size 56184 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_link1_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_link1_collision.STL new file mode 100644 index 000000000..3a73638f8 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_link1_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b5901c224dd0b657e62d724c6b561ae0d77b8d955ae0454e8a93985c21d14472 +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_link2.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_link2.STL new file mode 100644 index 000000000..4dd29380e --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_link2.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4f01c61e81bb440c789d242c43d00c16c44590ad2a25f004f8783e7df80bb3db +size 51984 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_link2_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_link2_collision.STL new file mode 100644 index 000000000..d1fb211d1 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_link2_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6136648bc9de8049d130376b049cac1f7a2deeb30534e6aa75f6ae82f89ce51e +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_link3.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_link3.STL new file mode 100644 index 000000000..d6f2ea32b --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_link3.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:20052a953fdfda74337317d939c59762bb70bb52c206eb62c4fa77e846ca948a +size 91284 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_link3_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_link3_collision.STL new file mode 100644 index 000000000..d27acdca4 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_link3_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3f8e87de06a09b9451352f870bfcd3cc4120676eb5aa108aa21409c03544026c +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_link4.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_link4.STL new file mode 100644 index 000000000..731214cd3 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_link4.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9355ba84b24102ccb8251a2213c63f48acf98dce47865b4ff446cec7c33c38cc +size 54884 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_link4_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_link4_collision.STL new file mode 100644 index 000000000..dd97e3894 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_link4_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3426b242ab5c4b8bba7e331a46eeaacba6094797d1fbdb4cff74cfc2cb1a564a +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_tip_link.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_tip_link.STL new file mode 100644 index 000000000..9d38ea072 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_tip_link.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:32636af3449202bc1689432c2f87eb5fa74b1b7a423a03aa6f850a7c829b8ea0 +size 112784 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_tip_link_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_tip_link_collision.STL new file mode 100644 index 000000000..565c9abd5 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_tip_link_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ea5df32f12ccb000510c2a96ea9adbf0c4be6f2b88ee0bcdbc88a5e1fc609f22 +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/palm_link.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/palm_link.STL new file mode 100644 index 000000000..213695039 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/palm_link.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cd1dfefc9be806bda6839cc1c787ed6e6c7cdd0773c0cf91d543877ac6a60aff +size 359284 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/palm_link_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/palm_link_collision.STL new file mode 100644 index 000000000..bd39ef195 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/palm_link_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1c5d4fe26c033a0c7820556a438825d9c206a0d63a5758e8e10ccaaf5269076f +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_link1.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_link1.STL new file mode 100644 index 000000000..bc2da1cbe --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_link1.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0e32302a46cd00d6cc1e7fb03cc483988c639271708c2300a9aecc9de537e42b +size 366584 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_link1_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_link1_collision.STL new file mode 100644 index 000000000..e603495cd --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_link1_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2adeb7b286e96c57c354c0b4d2107aa7bbd63a58b47a2fce828f313722d8eae2 +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_link2.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_link2.STL new file mode 100644 index 000000000..47ebbd0fa --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_link2.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:080015cce41de31954e53bbfca45f37f0439e2e689bca49e1910c70efc4f70b7 +size 92684 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_link2_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_link2_collision.STL new file mode 100644 index 000000000..a18af74bb --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_link2_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:930360774dcefe897cafcf6145d042217a78f1b54ca98dc7a1017db889f18afc +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_link3.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_link3.STL new file mode 100644 index 000000000..baf64130f --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_link3.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:52eef283d29c617fe4457151685386324bddbccf1598b8a7fc675bb8f28dc85a +size 89184 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_link3_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_link3_collision.STL new file mode 100644 index 000000000..ba9a3b435 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_link3_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f2042ec400e5840bf1b3c22aebb7c50ffa466e0f23474908c40ab546eefdbc6c +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_link4.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_link4.STL new file mode 100644 index 000000000..219cb7859 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_link4.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f0fb321c4abfd4ab7437caeb116f1935b94d1510fd921222eb49ba6c5a57f057 +size 65984 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_link4_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_link4_collision.STL new file mode 100644 index 000000000..65641915f --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_link4_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:48eb6a1c4818d6be44c1264b069c06362a2edc0bb8ebacb70f195e1e3f4e9090 +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_tip_link.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_tip_link.STL new file mode 100644 index 000000000..822ade70c --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_tip_link.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:25929e0c28a5c65afe88dbccaa697664a2385f8ead02126731a7e1b956ec1dea +size 96084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_tip_link_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_tip_link_collision.STL new file mode 100644 index 000000000..3f650635f --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_tip_link_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1f76a0f6319bd664570fa2571e6a87db2b5b94a47045732daef486f2896dc220 +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_link1.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_link1.STL new file mode 100644 index 000000000..5652a26cc --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_link1.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:36e0471a0373a7634fc10911b33f3b30a5fe829f6526e656a911406cebf19731 +size 56184 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_link1_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_link1_collision.STL new file mode 100644 index 000000000..223ccd7f4 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_link1_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:55cf7973ab4d57119f6eb032704defddf7077a01022f99d6a9f95eaea7f28875 +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_link2.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_link2.STL new file mode 100644 index 000000000..e4ca87897 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_link2.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:56bb1972b8cb1d63d48aa18283148ff6760e387dc64fe963b11209e07e243511 +size 51984 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_link2_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_link2_collision.STL new file mode 100644 index 000000000..ea3f95cc0 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_link2_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:db3b779d3cf98615617f5554946205e9f07de5cd9853005305048f617dc96fdc +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_link3.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_link3.STL new file mode 100644 index 000000000..7e270dd43 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_link3.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:48d70e865e2b49139028511808ae3ad25c5cf06e36fe959c6c71b5263dfcbbde +size 91284 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_link3_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_link3_collision.STL new file mode 100644 index 000000000..12304a5c7 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_link3_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:24f3464762c5ab07b4c56d3df6576ee43cb63eab0938c6366f1153323c619aa5 +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_link4.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_link4.STL new file mode 100644 index 000000000..b40fd6b23 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_link4.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3eede2c8e1eb1301bc23b4b453fb7014ca5965611b44b115a7330bf33e4d922e +size 54884 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_link4_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_link4_collision.STL new file mode 100644 index 000000000..bbfd834fc --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_link4_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a8708b0b03aab6a7fb5074a6c9639dee85be56415018ad16a9a04b6f43b6cc85 +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_tip_link.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_tip_link.STL new file mode 100644 index 000000000..59e46226a --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_tip_link.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:496039ce3f4f3fbe6bac466a01567797f741af95c6f11dc61626ec2de175900b +size 112784 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_tip_link_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_tip_link_collision.STL new file mode 100644 index 000000000..b5fede500 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_tip_link_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:53d0ef5c6346f3f5dd514646df784fa1b6b56c088164496ef661019566028c1d +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_link1.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_link1.STL new file mode 100644 index 000000000..71d4a7917 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_link1.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:558fac18be219c0e6bb6224b82c711b3efe762b547818ea074eda93dc38bad07 +size 56184 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_link1_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_link1_collision.STL new file mode 100644 index 000000000..528724ca1 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_link1_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:698e2327acfe737e11fb10f2fb1d36bbf8f1bc6e688f1e31e6232bfc51127581 +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_link2.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_link2.STL new file mode 100644 index 000000000..c1f3f753f --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_link2.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:338db71e2c1ec63fba860d5d2af52bcb1daff72349b0e03a45f82ae63539bd43 +size 51984 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_link2_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_link2_collision.STL new file mode 100644 index 000000000..ac71baf46 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_link2_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:50e5f990401d1ede299adc2034c5c2c66f8b2499a8f7c38d9bbc4e26da06c3b9 +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_link3.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_link3.STL new file mode 100644 index 000000000..16cd3a6fb --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_link3.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3af337085d5f6430a471462c18b9132f1447eb8c0f3c68a8b1e69318ff8c3db8 +size 91284 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_link3_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_link3_collision.STL new file mode 100644 index 000000000..29fe2892c --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_link3_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ddbd96f0b25ac9582a786bc6da7b363be61804e2b02fa5da4ca17c04f1715e5f +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_link4.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_link4.STL new file mode 100644 index 000000000..8d3580bfe --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_link4.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3b8efa21bafcae21119f243decf924dac51cc55181ed540e69ce6b85ecf03790 +size 54884 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_link4_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_link4_collision.STL new file mode 100644 index 000000000..b3a3fbc83 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_link4_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fafdff7b5e8b736bde927d6b62a7c953bd1f4bf8e77b442f5a7217ae0f2fa431 +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_tip_link.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_tip_link.STL new file mode 100644 index 000000000..3fef1c5f6 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_tip_link.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7a99f65a6a473899913d4273e61319e3724fbf81ac8532969ed5c2ff1c1bde54 +size 112784 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_tip_link_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_tip_link_collision.STL new file mode 100644 index 000000000..40cca7b11 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_tip_link_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7e855b092faf2e2f0471109814ad9b2469485f88a47b6abd290b79b0f663ab2d +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_link1.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_link1.STL new file mode 100644 index 000000000..db6a65028 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_link1.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:508321fefaeae1ae0567f89acd2492fac27cc468a2f562d895afcff192b0d00c +size 56184 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_link1_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_link1_collision.STL new file mode 100644 index 000000000..10c8ab9c5 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_link1_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc3b2c4d5b6304520c4a0297978ffaed3005d99cd8bdf74cc631deb6f5d9f14b +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_link2.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_link2.STL new file mode 100644 index 000000000..e2f06532f --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_link2.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e0b28769389c33af6af73c6dbf506be126570fa6ca7cbaf08f85a51005c48cde +size 51984 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_link2_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_link2_collision.STL new file mode 100644 index 000000000..6aea7ff97 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_link2_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a68648fc10cccdc58b2db7a4d09079ba2a9fa102a04e3c6a955e13e25edb2c30 +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_link3.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_link3.STL new file mode 100644 index 000000000..5f079cde3 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_link3.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b01668bab452260954faf8cbff94fb59c55ccb3e490ff6eb4b73e81e550c787f +size 91284 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_link3_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_link3_collision.STL new file mode 100644 index 000000000..c42eacf98 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_link3_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8eb85ee78e3fd9e489a75e0385ea8493db0af0023286a005ac148cc26ff65b81 +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_link4.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_link4.STL new file mode 100644 index 000000000..a53f12a70 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_link4.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d673bf02761f722728eb1ee0e7cc79ee6afba7693c78f623c3a4a89d5a905453 +size 54884 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_link4_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_link4_collision.STL new file mode 100644 index 000000000..bc7862bb4 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_link4_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:86783162be22f7600503a0a62484314367cf0d1adc2757a68681667e8f03205d +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_tip_link.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_tip_link.STL new file mode 100644 index 000000000..623387db6 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_tip_link.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e8b1fbee8e019afe490e820b1cba5e02c5124bb4a09aa73896bd46db6586dfde +size 113484 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_tip_link_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_tip_link_collision.STL new file mode 100644 index 000000000..e41cb1416 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_tip_link_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:29ed263d5e5cb24fdede93f6a8c695c36aaf5392f7c52acd4b13dee050a98bb9 +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_link1.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_link1.STL new file mode 100644 index 000000000..1356abb04 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_link1.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:85ce22b8285a7d85e4c866151c972cde0736d4f618f35015b0b96fe39e367e49 +size 56184 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_link1_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_link1_collision.STL new file mode 100644 index 000000000..39b4b4a4f --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_link1_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:011e4890f925b9d3454261f4fa98f0fb1b95c3aaeec707aa3db48a305fc42243 +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_link2.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_link2.STL new file mode 100644 index 000000000..594035022 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_link2.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9245362396a184e63af8d2707dc85a3250f1d7db4eeb079d5b3ee9b432939dd6 +size 51984 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_link2_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_link2_collision.STL new file mode 100644 index 000000000..8a5064314 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_link2_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bf966538aa20a9ea1ba6e58c8bf1bf09aef3ca6826f349757077d3de84ed0119 +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_link3.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_link3.STL new file mode 100644 index 000000000..e87500b5a --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_link3.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1083dcec6df545d60c3f14af1113613ca89cc8964692dd624baa750491915f40 +size 91284 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_link3_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_link3_collision.STL new file mode 100644 index 000000000..76abe5831 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_link3_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:26b793372d5232de0b82dd2e74a041564f9bb22394705f2bae23366921cc56b0 +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_link4.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_link4.STL new file mode 100644 index 000000000..d8604709d --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_link4.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6342d4562a8bfaef740850c02dc5418ade11dcbe04736b957d93522995080f35 +size 54884 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_link4_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_link4_collision.STL new file mode 100644 index 000000000..8ba27a01e --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_link4_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8643d9c3d6363aa7d6652e2137b4f496a1bd350dc39a0ddcc9cc8bfdc5f34093 +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_tip_link.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_tip_link.STL new file mode 100644 index 000000000..ccd435d94 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_tip_link.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ebbf48ca7ba3bca5f329f95277ae392d0c1a09615eda23f1bf646c5112a38cee +size 112784 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_tip_link_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_tip_link_collision.STL new file mode 100644 index 000000000..5a4d45b41 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_tip_link_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:4678873a3d1ca7c0a97f68cf87ebaa334484cf2d202da76b961e7c16a7a5968d +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/palm_link.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/palm_link.STL new file mode 100644 index 000000000..7a28ced65 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/palm_link.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1ba4495e3b7adc707d50150a8625a0ed3731fa42ab6d40e62ae7437da103ce5e +size 456184 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/palm_link_collision.STL b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/palm_link_collision.STL new file mode 100644 index 000000000..e8fe80cab --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/palm_link_collision.STL @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:542614615dc46aee7df9cad628374a3aea4c07f58e6f2c716d2b4cbebe665139 +size 25084 diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/urdf/left.urdf b/examples/g1_wuji_teleop/assets/wuji_hand/urdf/left.urdf new file mode 100644 index 000000000..560769b35 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/urdf/left.urdf @@ -0,0 +1,1472 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/g1_wuji_teleop/assets/wuji_hand/urdf/right.urdf b/examples/g1_wuji_teleop/assets/wuji_hand/urdf/right.urdf new file mode 100644 index 000000000..a29f29f10 --- /dev/null +++ b/examples/g1_wuji_teleop/assets/wuji_hand/urdf/right.urdf @@ -0,0 +1,1472 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/g1_wuji_teleop/config/avp_manus.yaml b/examples/g1_wuji_teleop/config/avp_manus.yaml new file mode 100644 index 000000000..1c61e2310 --- /dev/null +++ b/examples/g1_wuji_teleop/config/avp_manus.yaml @@ -0,0 +1,46 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +session: + app_name: G1WujiAvpManusTeleop + +manus_plugin: + plugin_name: manus_hand_plugin + plugin_root_id: manus + search_paths: + - ../../../install/plugins + args: [] + +robot: + variant: g1_wuji + initial_world_position: [2.0, 2.0, 0.8] + initial_world_orientation_xyzw: [0.0, 1.0, 0.0, 0.0] + + avp_frame_binding: + enabled: true + calibration_window_s: 3.0 + robot_reference_body: torso_link + robot_reference_offset_pos: [0.0077774, 0.0000210, 0.3836842] + robot_reference_offset_quat_xyzw: [0.0, 0.0, 0.0, 1.0] + head_forward_axis_isaac: [0.0, 1.0, 0.0] + + head_view_camera: + enabled: true + activate_on_calibration: true + prim_path: /World/TeleopTargets/HeadViewCamera + head_link_prim_path: /World/G1Wuji/torso_link/head_link + translation_offset_m: [0.05, 0.0, 0.3836842] + orientation_offset_rpy_deg: [70.0, 0.0, -90.0] + horizontal_aperture_mm: 20.955 + focal_length_mm: 5.0 + clipping_range_m: [0.01, 1000.0] + + third_viewpoint: + relative_position_m: [0.0, -1.2, 0.55] + +avp: + ee_pose_source: hand_tracking + ee_hand_joint: wrist + +retargeting: + backend: official_wuji diff --git a/examples/g1_wuji_teleop/config/vr_manus.yaml b/examples/g1_wuji_teleop/config/vr_manus.yaml new file mode 100644 index 000000000..650d19e42 --- /dev/null +++ b/examples/g1_wuji_teleop/config/vr_manus.yaml @@ -0,0 +1,50 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +session: + app_name: G1WujiVrManusTeleop + +manus_plugin: + plugin_name: manus_hand_plugin + plugin_root_id: manus + search_paths: + - ../../../install/plugins + args: [] + +robot: + variant: g1_wuji + initial_world_position: [2.0, 2.0, 0.8] + initial_world_orientation_xyzw: [0.0, 1.0, 0.0, 0.0] + + avp_frame_binding: + enabled: true + calibration_window_s: 3.0 + robot_reference_body: torso_link + robot_reference_offset_pos: [0.0077774, 0.0000210, 0.3836842] + robot_reference_offset_quat_xyzw: [0.0, 0.0, 0.0, 1.0] + head_forward_axis_isaac: [0.0, 1.0, 0.0] + + head_view_camera: + enabled: true + activate_on_calibration: true + prim_path: /World/TeleopTargets/HeadViewCamera + head_link_prim_path: /World/G1Wuji/torso_link/head_link + translation_offset_m: [0.05, 0.0, 0.3836842] + orientation_offset_rpy_deg: [70.0, 0.0, -90.0] + horizontal_aperture_mm: 20.955 + focal_length_mm: 5.0 + clipping_range_m: [0.01, 1000.0] + + third_viewpoint: + relative_position_m: [0.0, -1.2, 0.55] + +avp: + ee_pose_source: controller + controller_pose_source: grip + left_controller_to_palm_pos: [-0.08, 0.0, -0.02] + left_controller_to_palm_quat_xyzw: [0.0, 0.0, 0.382683, 0.92388] + right_controller_to_palm_pos: [0.08, 0.0, -0.02] + right_controller_to_palm_quat_xyzw: [0.0, 0.0, -0.382683, 0.92388] + +retargeting: + backend: official_wuji diff --git a/examples/g1_wuji_teleop/config/wuji_retargeting/left.yaml b/examples/g1_wuji_teleop/config/wuji_retargeting/left.yaml new file mode 100644 index 000000000..9b72954c6 --- /dev/null +++ b/examples/g1_wuji_teleop/config/wuji_retargeting/left.yaml @@ -0,0 +1,39 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# G1-Wuji MANUS retargeting config backed by the official wuji-retargeting package. + +optimizer: + type: "AdaptiveOptimizerAnalytical" + urdf_path: "../../assets/wuji_hand/urdf/left.urdf" + +retarget: + huber_delta: 2.0 + huber_delta_dir: 0.5 + norm_delta: 0.04 + + w_pos: 1.0 + w_dir: 2.0 + scaling: 1.0 + project_tip_dir: false + + w_full_hand: 1.0 + segment_scaling: + thumb: [0.98, 1.0, 1.0] + index: [1.1, 0.989, 1.03] + middle: [1.10, 0.96, 0.96] + ring: [1.1, 0.97, 0.99] + pinky: [1.105, 1.15, 1.15] + + pinch_thresholds: + index: { d1: 2.0, d2: 4.0 } + middle: { d1: 2.0, d2: 4.0 } + ring: { d1: 2.0, d2: 4.0 } + pinky: { d1: 2.0, d2: 4.0 } + + mediapipe_rotation: + x: 0.0 + y: 0.0 + z: 15.0 + + lp_alpha: 0.2 diff --git a/examples/g1_wuji_teleop/config/wuji_retargeting/right.yaml b/examples/g1_wuji_teleop/config/wuji_retargeting/right.yaml new file mode 100644 index 000000000..5a3f7f8b4 --- /dev/null +++ b/examples/g1_wuji_teleop/config/wuji_retargeting/right.yaml @@ -0,0 +1,39 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# G1-Wuji MANUS retargeting config backed by the official wuji-retargeting package. + +optimizer: + type: "AdaptiveOptimizerAnalytical" + urdf_path: "../../assets/wuji_hand/urdf/right.urdf" + +retarget: + huber_delta: 2.0 + huber_delta_dir: 0.5 + norm_delta: 0.04 + + w_pos: 1.0 + w_dir: 2.0 + scaling: 1.0 + project_tip_dir: false + + w_full_hand: 1.0 + segment_scaling: + thumb: [0.98, 1.0, 1.0] + index: [1.1, 0.989, 1.03] + middle: [1.10, 0.96, 0.96] + ring: [1.1, 0.97, 0.99] + pinky: [1.105, 1.15, 1.15] + + pinch_thresholds: + index: { d1: 2.0, d2: 4.0 } + middle: { d1: 2.0, d2: 4.0 } + ring: { d1: 2.0, d2: 4.0 } + pinky: { d1: 2.0, d2: 4.0 } + + mediapipe_rotation: + x: 0.0 + y: 0.0 + z: -15.0 + + lp_alpha: 0.2 diff --git a/examples/g1_wuji_teleop/docs/setup.md b/examples/g1_wuji_teleop/docs/setup.md new file mode 100644 index 000000000..4aa040241 --- /dev/null +++ b/examples/g1_wuji_teleop/docs/setup.md @@ -0,0 +1,125 @@ + + +# G1-Wuji AVP/VR + MANUS Setup + +This runbook assumes an Isaac Lab Python environment and an IsaacTeleop build +using the same Python version. + +Replace `/path/to/IsaacTeleop` and `/path/to/wuji-retargeting` with local paths. + +## 1. Build and Install IsaacTeleop + +```bash +cd /path/to/IsaacTeleop +conda activate isaacteleop + +cmake -S . -B build \ + -DCMAKE_BUILD_TYPE=Release \ + -DISAAC_TELEOP_PYTHON_VERSION=3.12 \ + -DBUILD_VIZ=OFF \ + -DBUILD_PLUGIN_OAK_CAMERA=OFF \ + -DENABLE_CLANG_FORMAT_CHECK=OFF \ + -DENABLE_CLOUDXR_BUNDLE_CHECK=ON + +cmake --build build --parallel "$(nproc)" +cmake --install build + +python -m pip install -r src/core/python/requirements-retargeters.txt +python -m pip install -r src/core/python/requirements-cloudxr.txt +python -m pip install --force-reinstall install/wheels/isaacteleop-*.whl +``` + +## 2. Install Wuji Retargeting + +Keep Wuji's repository outside IsaacTeleop. Install it into the same Python +environment used to run this example. + +```bash +cd /path/to +git clone --recurse-submodules https://github.com/wuji-technology/wuji-retargeting.git + +cd /path/to/wuji-retargeting +python -m pip install -U pip +python -m pip install -r requirements.txt +python -m pip install -e . + +python -c "from wuji_retargeting.retarget import Retargeter; print(Retargeter)" +``` + +## 3. Install MANUS Support + +Run the udev step on the host. + +```bash +cd /path/to/IsaacTeleop/src/plugins/manus +./install_udev_rules.sh +./install_manus.sh +``` + +Unplug and replug the MANUS dongle. The example configs expect the plugin under +`install/plugins`; edit `manus_plugin.search_paths` if your install prefix is +different. + +## 4. Start CloudXR Runtime + +Keep this terminal running. + +```bash +cd /path/to/IsaacTeleop +conda activate isaacteleop + +echo 'NV_DEVICE_PROFILE=auto-webrtc' > custom.env +python -m isaacteleop.cloudxr --cloudxr-env-config=./custom.env --accept-eula +``` + +On the headset, open the Isaac XR Teleop Sample Client and connect to the Ubuntu +host LAN IP. + +## 5. Verify MANUS Input + +Run this in a second terminal after CloudXR creates `~/.cloudxr/run/cloudxr.env`. + +```bash +cd /path/to/IsaacTeleop +source ~/.cloudxr/run/cloudxr.env + +./install/bin/manus_hand_tracker_printer +``` + +Close the printer before starting teleop because the MANUS SDK is single-owner. + +## 6. Run G1-Wuji Teleop + +AVP hand-wrist arm control: + +```bash +cd /path/to/IsaacTeleop +source ~/.cloudxr/run/cloudxr.env +conda activate isaacteleop + +# export LD_LIBRARY_PATH="$CONDA_PREFIX/lib/python3.12/site-packages/nvidia/cu13/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" +PYTHONPATH=examples/g1_wuji_teleop/python python -m g1_wuji_teleop \ + --config examples/g1_wuji_teleop/config/avp_manus.yaml \ + --device cuda:0 \ + --viz kit +``` + +VR/PICO controller arm control: + +```bash +cd /path/to/IsaacTeleop +source ~/.cloudxr/run/cloudxr.env +conda activate isaacteleop + +export LD_LIBRARY_PATH="$CONDA_PREFIX/lib/python3.12/site-packages/nvidia/cu13/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" +PYTHONPATH=examples/g1_wuji_teleop/python python -m g1_wuji_teleop \ + --config examples/g1_wuji_teleop/config/vr_manus.yaml \ + --device cuda:0 \ + --viz kit +``` + +Press `B` in the Isaac Sim window to start calibration and arm teleop. Press `R` +to reset the robot and recalibrate. diff --git a/examples/g1_wuji_teleop/python/g1_wuji_teleop/__init__.py b/examples/g1_wuji_teleop/python/g1_wuji_teleop/__init__.py new file mode 100644 index 000000000..b1e8db1a5 --- /dev/null +++ b/examples/g1_wuji_teleop/python/g1_wuji_teleop/__init__.py @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""G1-Wuji AVP/VR + MANUS teleoperation example.""" + +from .app import main + +__all__ = ["main"] diff --git a/examples/g1_wuji_teleop/python/g1_wuji_teleop/__main__.py b/examples/g1_wuji_teleop/python/g1_wuji_teleop/__main__.py new file mode 100644 index 000000000..520a69737 --- /dev/null +++ b/examples/g1_wuji_teleop/python/g1_wuji_teleop/__main__.py @@ -0,0 +1,12 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Run the G1-Wuji teleop example as a module.""" + +from __future__ import annotations + +from .app import main + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/g1_wuji_teleop/python/g1_wuji_teleop/app.py b/examples/g1_wuji_teleop/python/g1_wuji_teleop/app.py new file mode 100644 index 000000000..accafb6a3 --- /dev/null +++ b/examples/g1_wuji_teleop/python/g1_wuji_teleop/app.py @@ -0,0 +1,1530 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Minimal Isaac Lab scene for AVP + MANUS teleop on G1-Wuji. + +Run this script with an Isaac Sim / Isaac Lab Python environment. It intentionally +keeps the simulation side small: a ground plane, a light, the selected G1 USD, +and live hand joint targets from ``TeleopMain``. +""" + +from __future__ import annotations + +import argparse +import asyncio +import sys +import time +from collections.abc import Mapping, Sequence +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any + +import numpy as np + +from .config import default_config_path, example_path, load_yaml_file +from .input_stream import TeleopMain +from .robot import ( + AvpRobotFrameBinding, + HandTargetPostProcessor, + WujiHandTargetBackend, + as_torch, + avp_frame_binding_runtime_config, + matrix_to_quat_xyzw, + normalize_quat_xyzw, + openxr_pose_to_isaac_pose, + robot_profile_from_config, + wuji_hand_runtime_config, +) +from .scene import ( + G1WujiSceneConfig, + design_g1_wuji_scene, +) + + +ARM_JOINTS = { + "left": ( + "left_shoulder_pitch_joint", + "left_shoulder_roll_joint", + "left_shoulder_yaw_joint", + "left_elbow_joint", + "left_wrist_roll_joint", + "left_wrist_pitch_joint", + "left_wrist_yaw_joint", + ), + "right": ( + "right_shoulder_pitch_joint", + "right_shoulder_roll_joint", + "right_shoulder_yaw_joint", + "right_elbow_joint", + "right_wrist_roll_joint", + "right_wrist_pitch_joint", + "right_wrist_yaw_joint", + ), +} + + +@dataclass(frozen=True) +class HeadViewCameraConfig: + enabled: bool = True + activate_on_calibration: bool = True + prim_path: str = "/World/TeleopTargets/HeadViewCamera" + head_link_prim_path: str = "/World/G1Wuji/torso_link/head_link" + translation_offset_m: tuple[float, float, float] = (0.0, 0.0, 0.0) + orientation_offset_rpy_deg: tuple[float, float, float] = (90.0, 0.0, 90.0) + horizontal_aperture_mm: float = 20.955 + focal_length_mm: float = 18.14756 + clipping_range_m: tuple[float, float] = (0.01, 1000.0) + + +@dataclass(frozen=True) +class ThirdViewpointConfig: + relative_position_m: tuple[float, float, float] = (0.0, -1.2, 0.55) + + +class HeadViewCameraController: + """Drive a viewport camera from the robot head pose after AVP calibration.""" + + def __init__( + self, + *, + stage: Any, + UsdGeom: Any, + Gf: Any, + viewport_api: Any, + config: HeadViewCameraConfig, + head_link_prim: Any | None, + avp_frame_binding: Any | None, + ) -> None: + self._config = config + self._viewport_api = viewport_api + self._UsdGeom = UsdGeom + self._Gf = Gf + self._head_link_prim = head_link_prim + self._avp_frame_binding = avp_frame_binding + self._active = False + self._announced_missing_head = False + self._orientation_offset_quat = self._orientation_offset_quatd(config) + + camera = UsdGeom.Camera.Define(stage, config.prim_path) + self._camera_prim = camera.GetPrim() + self._camera_xform = UsdGeom.Xformable(self._camera_prim) + self._translate_op = self._camera_xform.AddTranslateOp() + # USD xform op value types are strict. Head prim world rotations come back as + # GfQuatd here, so keep the camera orient op in double precision as well. + self._orient_op = self._camera_xform.AddOrientOp( + precision=UsdGeom.XformOp.PrecisionDouble + ) + self._scale_op = self._camera_xform.AddScaleOp() + self._camera_path = str(self._camera_prim.GetPath()) + + camera.GetHorizontalApertureAttr().Set(float(config.horizontal_aperture_mm)) + camera.GetFocalLengthAttr().Set(float(config.focal_length_mm)) + camera.GetClippingRangeAttr().Set( + Gf.Vec2f( + float(config.clipping_range_m[0]), + float(config.clipping_range_m[1]), + ) + ) + self._scale_op.Set(Gf.Vec3f(1.0, 1.0, 1.0)) + + @property + def camera_path(self) -> str: + return self._camera_path + + def activate(self) -> None: + if self._viewport_api is None: + return + self._viewport_api.camera_path = self._camera_path + self._active = True + + def deactivate(self) -> None: + self._active = False + + def update(self, *, xform_cache: Any) -> None: + head_link_prim = self._head_link_prim + if head_link_prim is None or xform_cache is None: + if not self._announced_missing_head: + print( + "Head view camera is enabled but no robot head prim is available; " + "camera updates are disabled.", + flush=True, + ) + self._announced_missing_head = True + return + + xform_cache.Clear() + world_transform = xform_cache.GetLocalToWorldTransform(head_link_prim) + world_translation = world_transform.ExtractTranslation() + world_rotation = world_transform.ExtractRotationQuat() + camera_base_rotation = world_rotation * self._orientation_offset_quat + camera_rotation = self._apply_head_pose_delta(camera_base_rotation) + + offset = self._config.translation_offset_m + offset_vec = self._Gf.Vec3d( + float(offset[0]), float(offset[1]), float(offset[2]) + ) + camera_translation = world_translation + world_transform.TransformDir( + offset_vec + ) + + self._translate_op.Set(camera_translation) + self._orient_op.Set(camera_rotation) + + def _apply_head_pose_delta(self, base_rotation: Any) -> Any: + avp_frame_binding = self._avp_frame_binding + if avp_frame_binding is None or not avp_frame_binding.calibrated: + return base_rotation + tilt_delta_rot = avp_frame_binding.latest_head_tilt_delta_rot() + camera_rotation = base_rotation + if tilt_delta_rot is not None: + tilt_delta_quat = self._matrix3_to_quatd(tilt_delta_rot) + camera_rotation = tilt_delta_quat * camera_rotation + return camera_rotation + + def _orientation_offset_quatd(self, config: HeadViewCameraConfig) -> Any: + roll_deg, pitch_deg, yaw_deg = config.orientation_offset_rpy_deg + roll_rad = np.deg2rad(float(roll_deg)) + pitch_rad = np.deg2rad(float(pitch_deg)) + yaw_rad = np.deg2rad(float(yaw_deg)) + + cr = np.cos(roll_rad * 0.5) + sr = np.sin(roll_rad * 0.5) + cp = np.cos(pitch_rad * 0.5) + sp = np.sin(pitch_rad * 0.5) + cy = np.cos(yaw_rad * 0.5) + sy = np.sin(yaw_rad * 0.5) + + quat_xyzw = normalize_quat_xyzw( + ( + sr * cp * cy - cr * sp * sy, + cr * sp * cy + sr * cp * sy, + cr * cp * sy - sr * sp * cy, + cr * cp * cy + sr * sp * sy, + ) + ) + return self._Gf.Quatd( + float(quat_xyzw[3]), + self._Gf.Vec3d( + float(quat_xyzw[0]), + float(quat_xyzw[1]), + float(quat_xyzw[2]), + ), + ) + + def _matrix3_to_quatd(self, matrix: np.ndarray) -> Any: + quat_xyzw = normalize_quat_xyzw( + matrix_to_quat_xyzw(np.asarray(matrix, dtype=np.float64)) + ) + return self._Gf.Quatd( + float(quat_xyzw[3]), + self._Gf.Vec3d( + float(quat_xyzw[0]), + float(quat_xyzw[1]), + float(quat_xyzw[2]), + ), + ) + + +class ThirdViewpointController: + """Drive a right-side viewport viewpoint from the robot reference pose.""" + + _VIEWPOINT_PATH = "/OmniverseKit_Persp" + _WINDOW_TITLE = "G1-Wuji Third View" + + def __init__( + self, + *, + Gf: Any, + ViewportManager: Any, + viewport_utility: Any, + ui: Any, + config: ThirdViewpointConfig, + reference_prim: Any, + main_viewport_api: Any, + main_camera_path: str | None, + status_label: str, + ) -> None: + self._Gf = Gf + self._ViewportManager = ViewportManager + self._viewport_utility = viewport_utility + self._ui = ui + self._config = config + self._reference_prim = reference_prim + self._main_viewport_api = main_viewport_api + self._main_camera_path = ( + None if main_camera_path is None else str(main_camera_path) + ) + self._status_label = status_label + self._window = None + self._viewport_api = None + self._active = False + self._layout_requested = False + + def activate(self, *, xform_cache: Any) -> None: + if self._active: + return + if self._ensure_window() is None: + return + self._set_window_camera() + self._set_initial_view(xform_cache=xform_cache) + self._set_main_camera() + self._active = True + + def deactivate(self) -> None: + self._active = False + if self._window is not None: + self._window.visible = False + + def _set_initial_view(self, *, xform_cache: Any) -> None: + xform_cache.Clear() + world_transform = xform_cache.GetLocalToWorldTransform(self._reference_prim) + target = world_transform.ExtractTranslation() + offset = self._config.relative_position_m + eye = target + world_transform.TransformDir( + self._Gf.Vec3d(float(offset[0]), float(offset[1]), float(offset[2])) + ) + self._ViewportManager.set_camera_view( + self._VIEWPOINT_PATH, + eye=self._vec3_to_list(eye), + target=self._vec3_to_list(target), + ) + + def _ensure_window(self) -> Any: + if self._window is not None: + self._window.visible = True + return self._window + + self._window = self._viewport_utility.create_viewport_window( + self._WINDOW_TITLE, + width=1280, + height=720, + camera_path=self._VIEWPOINT_PATH, + ) + if self._window is None: + print( + f"[{self._status_label}] failed to create third viewpoint window.", + flush=True, + ) + return None + + self._viewport_api = self._window.viewport_api + self._set_window_camera() + self._window.visible = True + self._dock_to_main_viewport_async() + return self._window + + def _dock_to_main_viewport_async(self) -> None: + window = self._window + if window is None or self._layout_requested: + return + self._layout_requested = True + + async def dock_window() -> None: + import omni.kit.app + + await omni.kit.app.get_app().next_update_async() + target_window = self._ui.Workspace.get_window("Viewport") + if target_window is not None and window is not None: + window.dock_in(target_window, self._ui.DockPosition.RIGHT, 0.5) + self._set_window_camera() + self._set_main_camera() + + asyncio.ensure_future(dock_window()) + + def _set_main_camera(self) -> None: + if self._main_viewport_api is None or not self._main_camera_path: + return + self._main_viewport_api.camera_path = self._main_camera_path + + def _set_window_camera(self) -> None: + if self._viewport_api is None: + return + self._viewport_api.camera_path = self._VIEWPOINT_PATH + + def _vec3_to_list(self, value: Any) -> list[float]: + return [float(value[0]), float(value[1]), float(value[2])] + + +def _head_view_camera_config(config: Mapping[str, Any]) -> HeadViewCameraConfig: + robot_config = config.get("robot", {}) + if robot_config is None: + robot_config = {} + if not isinstance(robot_config, Mapping): + raise ValueError("Config field 'robot' must be a mapping") + + camera_config = robot_config.get("head_view_camera", {}) + if camera_config is None: + camera_config = {} + if not isinstance(camera_config, Mapping): + raise ValueError("Config field 'robot.head_view_camera' must be a mapping") + + prim_path = str( + camera_config.get("prim_path", "/World/TeleopTargets/HeadViewCamera") + ).strip() + if not prim_path: + raise ValueError( + "Config field 'robot.head_view_camera.prim_path' must be non-empty" + ) + head_link_prim_path = str( + camera_config.get( + "head_link_prim_path", + "/World/G1Wuji/torso_link/head_link", + ) + ).strip() + if not head_link_prim_path: + raise ValueError( + "Config field 'robot.head_view_camera.head_link_prim_path' must be non-empty" + ) + + return HeadViewCameraConfig( + enabled=bool(camera_config.get("enabled", True)), + activate_on_calibration=bool( + camera_config.get("activate_on_calibration", True) + ), + prim_path=prim_path, + head_link_prim_path=head_link_prim_path, + translation_offset_m=_float_tuple( + camera_config.get("translation_offset_m"), + name="robot.head_view_camera.translation_offset_m", + length=3, + default=(0.0, 0.0, 0.0), + ), + orientation_offset_rpy_deg=_float_tuple( + camera_config.get("orientation_offset_rpy_deg"), + name="robot.head_view_camera.orientation_offset_rpy_deg", + length=3, + default=(90.0, 0.0, 90.0), + ), + horizontal_aperture_mm=float( + camera_config.get("horizontal_aperture_mm", 20.955) + ), + focal_length_mm=float(camera_config.get("focal_length_mm", 18.14756)), + clipping_range_m=_float_tuple( + camera_config.get("clipping_range_m"), + name="robot.head_view_camera.clipping_range_m", + length=2, + default=(0.01, 1000.0), + ), + ) + + +def _third_viewpoint_config(config: Mapping[str, Any]) -> ThirdViewpointConfig | None: + robot_config = config.get("robot", {}) + if robot_config is None: + robot_config = {} + if not isinstance(robot_config, Mapping): + raise ValueError("Config field 'robot' must be a mapping") + if "third_viewpoint" not in robot_config: + return None + + viewpoint_config = robot_config.get("third_viewpoint", {}) + if viewpoint_config is None: + viewpoint_config = {} + if not isinstance(viewpoint_config, Mapping): + raise ValueError("Config field 'robot.third_viewpoint' must be a mapping") + + return ThirdViewpointConfig( + relative_position_m=_float_tuple( + viewpoint_config.get("relative_position_m"), + name="robot.third_viewpoint.relative_position_m", + length=3, + default=(0.0, -1.2, 0.55), + ) + ) + + +def _hide_third_viewpoint_window(viewport_utility: Any) -> None: + window = viewport_utility.get_active_viewport_window( + window_name=ThirdViewpointController._WINDOW_TITLE + ) + if window is not None: + window.visible = False + + +@dataclass(frozen=True) +class ArmIkSideConfig: + body_name: str + body_offset_pos: tuple[float, float, float] + body_offset_quat_xyzw: tuple[float, float, float, float] + target_orientation_correction_quat_xyzw: tuple[float, float, float, float] + joint_names: tuple[str, ...] + + +@dataclass(frozen=True) +class ArmIkRuntimeConfig: + status_label: str = "teleop" + enabled: bool = True + pose_scale: float = 1.0 + pose_z_offset: float = 0.0 + command_type: str = "pose" + startup_relative_position: bool = True + startup_relative_orientation: bool = True + position_error_scale: float = 1.0 + orientation_error_scale: float = 0.35 + position_deadband_m: float = 0.003 + orientation_deadband_rad: float = 0.06 + ik_method: str = "dls" + damping: float = 0.05 + smoothing_alpha: float = 0.35 + clamp_to_joint_limits: bool = True + sides: dict[str, ArmIkSideConfig] = field(default_factory=dict) + + +def _default_config_path() -> Path: + return default_config_path() + + +def _robot_initial_world_position( + config: Mapping[str, Any], +) -> tuple[float, float, float]: + robot_config = config.get("robot", {}) + if robot_config is None: + return (0.0, 0.0, 0.0) + if not isinstance(robot_config, Mapping): + raise ValueError("Config field 'robot' must be a mapping") + + return _float_tuple( + robot_config.get("initial_world_position"), + name="robot.initial_world_position", + length=3, + default=(0.0, 0.0, 0.0), + ) + + +def _robot_initial_world_orientation_xyzw( + config: Mapping[str, Any], +) -> tuple[float, float, float, float]: + robot_config = config.get("robot", {}) + if robot_config is None: + return (0.0, 0.0, 0.0, 1.0) + if not isinstance(robot_config, Mapping): + raise ValueError("Config field 'robot' must be a mapping") + + raw_quat = robot_config.get("initial_world_orientation_xyzw") + quat = _float_tuple( + raw_quat, + name="robot.initial_world_orientation_xyzw", + length=4, + default=(0.0, 0.0, 0.0, 1.0), + ) + norm = sum(value * value for value in quat) ** 0.5 + if norm < 1.0e-8: + raise ValueError( + "Config field 'robot.initial_world_orientation_xyzw' must be non-zero" + ) + return tuple(value / norm for value in quat) + + +def _float_tuple( + values: Any, + *, + name: str, + length: int, + default: Sequence[float], +) -> tuple[float, ...]: + raw = default if values is None else values + if not isinstance(raw, Sequence) or isinstance(raw, (str, bytes)): + raise ValueError(f"Config field {name!r} must be a {length}-element sequence") + if len(raw) != length: + raise ValueError(f"Config field {name!r} must contain exactly {length} values") + return tuple(float(value) for value in raw) + + +def _arm_ik_runtime_config(config: Mapping[str, Any]) -> ArmIkRuntimeConfig: + profile = robot_profile_from_config(config) + robot_config = config.get("robot", {}) + if robot_config is None: + robot_config = {} + if not isinstance(robot_config, Mapping): + raise ValueError("Config field 'robot' must be a mapping") + + # Keep the arm IK body bindings pinned in code rather than mirroring them in + # the teleop YAML. These offsets are asset-specific and tuned together. + sides = { + "left": ArmIkSideConfig( + body_name=profile.ik_body_names["left"], + body_offset_pos=profile.ik_body_offset_pos["left"], + body_offset_quat_xyzw=profile.ik_body_offset_quat_xyzw["left"], + target_orientation_correction_quat_xyzw=( + profile.ik_target_orientation_correction_quat_xyzw["left"] + ), + joint_names=ARM_JOINTS["left"], + ), + "right": ArmIkSideConfig( + body_name=profile.ik_body_names["right"], + body_offset_pos=profile.ik_body_offset_pos["right"], + body_offset_quat_xyzw=profile.ik_body_offset_quat_xyzw["right"], + target_orientation_correction_quat_xyzw=( + profile.ik_target_orientation_correction_quat_xyzw["right"] + ), + joint_names=ARM_JOINTS["right"], + ), + } + + return ArmIkRuntimeConfig( + status_label=profile.status_label, + enabled=True, + pose_scale=1.0, + pose_z_offset=0.0, + command_type="pose", + startup_relative_position=True, + startup_relative_orientation=True, + position_error_scale=0.8, + orientation_error_scale=0.8, + position_deadband_m=0.004, + orientation_deadband_rad=0.08, + ik_method="dls", + damping=0.05, + smoothing_alpha=0.80, + clamp_to_joint_limits=True, + sides=sides, + ) + + +def _load_isaac_lab(): + from isaaclab.app import AppLauncher + + return AppLauncher + + +class DualArmIkController: + """Drive G1 arm joints from left/right end-effector pose targets.""" + + def __init__( + self, + robot: Any, + config: ArmIkRuntimeConfig, + *, + device: str, + pose_binding: AvpRobotFrameBinding | None, + ) -> None: + import torch + from isaaclab.controllers import DifferentialIKController + from isaaclab.controllers.differential_ik_cfg import DifferentialIKControllerCfg + from isaaclab.utils import math as math_utils + + self._robot = robot + self._config = config + self._pose_binding = pose_binding + self._device = device + self._torch = torch + self._math_utils = math_utils + self._controllers: dict[str, Any] = {} + self._joint_ids: dict[str, Sequence[int] | slice] = {} + self._joint_id_lists: dict[str, list[int]] = {} + self._joint_names: dict[str, tuple[str, ...]] = {} + self._body_ids: dict[str, int] = {} + self._body_names: dict[str, str] = {} + self._jacobi_body_ids: dict[str, int] = {} + self._jacobi_joint_ids: dict[str, list[int]] = {} + self._offset_pos: dict[str, Any] = {} + self._offset_rot: dict[str, Any] = {} + self._target_rot_correction: dict[str, Any] = {} + self._last_targets: dict[str, Any] = {} + self._startup_target_refs: dict[str, tuple[Any, Any, Any, Any]] = {} + self._startup_ref_announced: set[str] = set() + + ik_params = self._ik_params() + for side, side_config in config.sides.items(): + joint_ids, resolved_joint_names = robot.find_joints( + list(side_config.joint_names), preserve_order=True + ) + missing = [ + name + for name in side_config.joint_names + if name not in resolved_joint_names + ] + if missing: + raise RuntimeError( + f"{self._config.status_label} USD is missing {side} arm joints: {missing[:8]}" + ) + + body_ids, body_names = robot.find_bodies(side_config.body_name) + if len(body_ids) != 1: + raise RuntimeError( + f"Expected one {side} IK body match for {side_config.body_name!r}; " + f"got {len(body_ids)}: {body_names}" + ) + + body_idx = int(body_ids[0]) + joint_id_list = [int(joint_id) for joint_id in joint_ids] + if robot.is_fixed_base: + jacobi_body_idx = body_idx - 1 + jacobi_joint_ids = joint_id_list + else: + jacobi_body_idx = body_idx + jacobi_joint_ids = [joint_id + 6 for joint_id in joint_id_list] + + controller_cfg = DifferentialIKControllerCfg( + command_type=config.command_type, + use_relative_mode=False, + ik_method=config.ik_method, + ik_params=ik_params, + ) + self._controllers[side] = DifferentialIKController( + cfg=controller_cfg, + num_envs=1, + device=device, + ) + self._joint_id_lists[side] = joint_id_list + self._joint_ids[side] = ( + slice(None) if len(joint_id_list) == robot.num_joints else joint_id_list + ) + self._joint_names[side] = tuple(str(name) for name in resolved_joint_names) + self._body_ids[side] = body_idx + self._body_names[side] = str(body_names[0]) + self._jacobi_body_ids[side] = int(jacobi_body_idx) + self._jacobi_joint_ids[side] = jacobi_joint_ids + self._offset_pos[side] = torch.tensor( + side_config.body_offset_pos, + dtype=torch.float32, + device=device, + ).unsqueeze(0) + self._offset_rot[side] = torch.tensor( + side_config.body_offset_quat_xyzw, + dtype=torch.float32, + device=device, + ).unsqueeze(0) + self._target_rot_correction[side] = self._math_utils.quat_unique( + self._math_utils.normalize( + torch.tensor( + side_config.target_orientation_correction_quat_xyzw, + dtype=torch.float32, + device=device, + ).unsqueeze(0) + ) + ) + + def _ik_params(self) -> dict[str, float]: + if self._config.ik_method == "dls": + return {"lambda_val": self._config.damping} + if self._config.ik_method == "pinv": + return {"k_val": 1.0} + if self._config.ik_method == "trans": + return {"k_val": 1.0} + if self._config.ik_method == "svd": + return {"k_val": 1.0, "min_singular_value": 1.0e-5} + return {} + + def _frame_pose_root(self, side: str) -> tuple[Any, Any]: + body_idx = self._body_ids[side] + ee_pos_w = as_torch(self._robot.data.body_pos_w)[:, body_idx] + ee_quat_w = as_torch(self._robot.data.body_quat_w)[:, body_idx] + root_pos_w = as_torch(self._robot.data.root_pos_w) + root_quat_w = as_torch(self._robot.data.root_quat_w) + ee_pos_b, ee_quat_b = self._math_utils.subtract_frame_transforms( + root_pos_w, root_quat_w, ee_pos_w, ee_quat_w + ) + return self._math_utils.combine_frame_transforms( + ee_pos_b, + ee_quat_b, + self._offset_pos[side], + self._offset_rot[side], + ) + + def _target_pose_root( + self, side: str, pose: Mapping[str, Any] + ) -> tuple[Any, Any] | None: + if self._pose_binding is not None: + target_pose = self._pose_binding.transform_pose(side, pose) + if target_pose is None: + return None + pos_w_np, quat_w_np = target_pose + else: + pos_w_np, quat_w_np = openxr_pose_to_isaac_pose( + pose, + scale=self._config.pose_scale, + offset_z=self._config.pose_z_offset, + ) + return self._target_pose_root_from_world_pose( + pos_w_np, + quat_w_np, + side=side, + apply_target_rot_correction=True, + ) + + def _target_pose_root_from_world_pose( + self, + pos_w_np: Sequence[float] | np.ndarray, + quat_w_np: Sequence[float] | np.ndarray, + *, + side: str | None = None, + apply_target_rot_correction: bool = False, + ) -> tuple[Any, Any]: + pos_w = self._torch.as_tensor( + pos_w_np, + dtype=self._torch.float32, + device=self._device, + ).unsqueeze(0) + quat_w = self._torch.as_tensor( + normalize_quat_xyzw(quat_w_np), + dtype=self._torch.float32, + device=self._device, + ).unsqueeze(0) + if apply_target_rot_correction: + if side is None: + raise ValueError( + "side is required when applying target orientation correction" + ) + quat_w = self._math_utils.normalize( + self._math_utils.quat_mul(quat_w, self._target_rot_correction[side]) + ) + else: + quat_w = self._math_utils.normalize(quat_w) + quat_w = self._math_utils.quat_unique(quat_w) + root_pos_w = as_torch(self._robot.data.root_pos_w) + root_quat_w = as_torch(self._robot.data.root_quat_w) + return self._math_utils.subtract_frame_transforms( + root_pos_w, root_quat_w, pos_w, quat_w + ) + + def _startup_relative_target( + self, + side: str, + target_pos: Any, + target_quat: Any, + ee_pos_curr: Any, + ee_quat_curr: Any, + ) -> tuple[Any, Any]: + if not ( + self._config.startup_relative_position + or self._config.startup_relative_orientation + ): + return target_pos, target_quat + + ref = self._startup_target_refs.get(side) + if ref is None: + ref = ( + target_pos.detach().clone(), + self._math_utils.quat_unique( + self._math_utils.normalize(target_quat.detach().clone()) + ), + ee_pos_curr.detach().clone(), + self._math_utils.quat_unique( + self._math_utils.normalize(ee_quat_curr.detach().clone()) + ), + ) + self._startup_target_refs[side] = ref + self._announce_startup_relative_target(side, ref) + + target_start_pos, target_start_quat, ee_start_pos, ee_start_quat = ref + adjusted_pos = target_pos + adjusted_quat = target_quat + + # AVP optical wrists and the robot EE rarely share a reliable absolute + # pose. Bind the first valid target to the current simulated EE, then + # command startup-relative translation and rotation deltas. + if self._config.startup_relative_position: + adjusted_pos = ee_start_pos + (target_pos - target_start_pos) + if self._config.startup_relative_orientation: + target_quat = self._math_utils.quat_unique( + self._math_utils.normalize(target_quat) + ) + quat_delta = self._math_utils.quat_mul( + target_quat, + self._math_utils.quat_inv(target_start_quat), + ) + adjusted_quat = self._math_utils.normalize( + self._math_utils.quat_mul(quat_delta, ee_start_quat) + ) + adjusted_quat = self._math_utils.quat_unique(adjusted_quat) + return adjusted_pos, adjusted_quat + + def _announce_startup_relative_target( + self, + side: str, + ref: tuple[Any, Any, Any, Any], + ) -> None: + if side in self._startup_ref_announced: + return + target_start_pos, target_start_quat, ee_start_pos, ee_start_quat = ref + print( + f"[{self._config.status_label}] {side} IK startup target reference calibrated " + f"relative_position={self._config.startup_relative_position} " + f"relative_orientation={self._config.startup_relative_orientation} " + f"target_pos={np.round(target_start_pos[0].detach().cpu().numpy(), 4).tolist()} " + f"ee_pos={np.round(ee_start_pos[0].detach().cpu().numpy(), 4).tolist()} " + f"target_quat_xyzw={np.round(target_start_quat[0].detach().cpu().numpy(), 4).tolist()} " + f"ee_quat_xyzw={np.round(ee_start_quat[0].detach().cpu().numpy(), 4).tolist()}", + flush=True, + ) + self._startup_ref_announced.add(side) + + def _compute_delta_joint_pos(self, delta_pose: Any, jacobian: Any) -> Any: + if self._config.ik_method == "pinv": + jacobian_pinv = self._torch.linalg.pinv(jacobian) + return (jacobian_pinv @ delta_pose.unsqueeze(-1)).squeeze(-1) + if self._config.ik_method == "svd": + min_singular_value = 1.0e-5 + U, S, Vh = self._torch.linalg.svd(jacobian) + S_inv = 1.0 / S + S_inv = self._torch.where( + min_singular_value < S, + S_inv, + self._torch.zeros_like(S_inv), + ) + jacobian_pinv = ( + self._torch.transpose(Vh, dim0=1, dim1=2)[:, :, : jacobian.shape[1]] + @ self._torch.diag_embed(S_inv) + @ self._torch.transpose(U, dim0=1, dim1=2) + ) + return (jacobian_pinv @ delta_pose.unsqueeze(-1)).squeeze(-1) + if self._config.ik_method == "trans": + jacobian_t = self._torch.transpose(jacobian, dim0=1, dim1=2) + return (jacobian_t @ delta_pose.unsqueeze(-1)).squeeze(-1) + if self._config.ik_method == "dls": + jacobian_t = self._torch.transpose(jacobian, dim0=1, dim1=2) + lambda_val = self._config.damping + lambda_matrix = (lambda_val**2) * self._torch.eye( + n=jacobian.shape[1], + device=self._device, + ) + return ( + jacobian_t + @ self._torch.inverse(jacobian @ jacobian_t + lambda_matrix) + @ delta_pose.unsqueeze(-1) + ).squeeze(-1) + raise ValueError( + f"Unsupported inverse-kinematics method: {self._config.ik_method}" + ) + + def _compute_joint_targets( + self, + side: str, + ee_pos_curr: Any, + ee_quat_curr: Any, + joint_pos: Any, + ) -> Any: + jacobian = self._frame_jacobian(side) + controller = self._controllers[side] + if self._config.command_type == "position": + return controller.compute( + ee_pos_curr, + ee_quat_curr, + jacobian, + joint_pos, + ) + + position_error, axis_angle_error = self._math_utils.compute_pose_error( + ee_pos_curr, + ee_quat_curr, + controller.ee_pos_des, + controller.ee_quat_des, + rot_error_type="axis_angle", + ) + if self._config.position_deadband_m > 0.0: + position_norm = self._torch.linalg.norm(position_error, dim=1, keepdim=True) + position_error = self._torch.where( + position_norm <= self._config.position_deadband_m, + self._torch.zeros_like(position_error), + position_error, + ) + if self._config.orientation_deadband_rad > 0.0: + orientation_norm = self._torch.linalg.norm( + axis_angle_error, dim=1, keepdim=True + ) + axis_angle_error = self._torch.where( + orientation_norm <= self._config.orientation_deadband_rad, + self._torch.zeros_like(axis_angle_error), + axis_angle_error, + ) + # Pose-mode wrist targets are much noisier in orientation than in translation. + # Down-weight angular error so small AVP wrist rotations do not yank the whole + # arm into a large reconfiguration before the translational target is reached. + position_error = position_error * self._config.position_error_scale + axis_angle_error = axis_angle_error * self._config.orientation_error_scale + pose_error = self._torch.cat((position_error, axis_angle_error), dim=1) + if bool(self._torch.all(self._torch.abs(pose_error) <= 1.0e-9)): + return joint_pos + delta_joint_pos = self._compute_delta_joint_pos(pose_error, jacobian) + return joint_pos + delta_joint_pos + + def _frame_jacobian(self, side: str): + jacobian = as_torch(self._robot.root_physx_view.get_jacobians())[ + :, self._jacobi_body_ids[side], :, self._jacobi_joint_ids[side] + ].clone() + root_quat_w = as_torch(self._robot.data.root_quat_w) + root_rot_b_w = self._math_utils.matrix_from_quat( + self._math_utils.quat_inv(root_quat_w) + ) + jacobian[:, 0:3, :] = self._torch.bmm(root_rot_b_w, jacobian[:, 0:3, :]) + jacobian[:, 3:, :] = self._torch.bmm(root_rot_b_w, jacobian[:, 3:, :]) + offset_pos = self._offset_pos[side] + offset_rot = self._offset_rot[side] + jacobian[:, 0:3, :] += self._torch.bmm( + -self._math_utils.skew_symmetric_matrix(offset_pos), + jacobian[:, 3:, :], + ) + jacobian[:, 3:, :] = self._torch.bmm( + self._math_utils.matrix_from_quat(offset_rot), + jacobian[:, 3:, :], + ) + return jacobian + + def _apply_target_pose_root( + self, + side: str, + target_pose_root: tuple[Any, Any], + ) -> None: + ee_pos_curr, ee_quat_curr = self._frame_pose_root(side) + target_pos, target_quat = target_pose_root + target_pos, target_quat = self._startup_relative_target( + side, + target_pos, + target_quat, + ee_pos_curr, + ee_quat_curr, + ) + command = ( + target_pos + if self._config.command_type == "position" + else self._torch.cat((target_pos, target_quat), dim=1) + ) + self._controllers[side].set_command(command, ee_pos_curr, ee_quat_curr) + joint_pos = as_torch(self._robot.data.joint_pos)[:, self._joint_ids[side]] + joint_targets = self._compute_joint_targets( + side, + ee_pos_curr, + ee_quat_curr, + joint_pos, + ) + + if self._config.clamp_to_joint_limits: + limits = as_torch(self._robot.data.soft_joint_pos_limits)[ + :, self._joint_ids[side], : + ] + joint_targets = self._torch.clamp( + joint_targets, limits[:, :, 0], limits[:, :, 1] + ) + + previous = self._last_targets.get(side) + if previous is not None and self._config.smoothing_alpha < 1.0: + alpha = self._config.smoothing_alpha + joint_targets = previous * (1.0 - alpha) + joint_targets * alpha + + self._last_targets[side] = joint_targets + self._robot.set_joint_position_target( + target=joint_targets, + joint_ids=self._joint_ids[side], + ) + + def update(self, sample: Mapping[str, Any]) -> None: + if not self._config.enabled: + return + + for side in ("left", "right"): + pose = sample["ee_poses"].get(side) + if pose is None: + target = self._last_targets.get(side) + if target is not None: + self._robot.set_joint_position_target( + target=target, + joint_ids=self._joint_ids[side], + ) + continue + + target_pose = self._target_pose_root(side, pose) + if target_pose is None: + continue + self._apply_target_pose_root(side, target_pose) + + def reset_startup_references(self) -> None: + self._startup_target_refs.clear() + self._startup_ref_announced.clear() + print( + f"[{self._config.status_label}] arm IK startup target references cleared; " + "waiting for re-arm calibration.", + flush=True, + ) + + +def _find_required_joint_ids( + robot: Any, + joint_names: Sequence[str], + side: str, + *, + status_label: str, +) -> list[int]: + joint_ids, resolved_names = robot.find_joints( + list(joint_names), preserve_order=True + ) + missing = [name for name in joint_names if name not in resolved_names] + if missing: + available = list(getattr(robot.data, "joint_names", []) or []) + raise RuntimeError( + f"{status_label} USD is missing {side} hand joints. " + f"Missing first entries: {missing[:8]}; available count={len(available)}" + ) + return list(joint_ids) + + +def _default_joint_positions( + robot: Any, + joint_ids: Sequence[int], +) -> np.ndarray: + default_joint_pos = as_torch(robot.data.default_joint_pos) + values = default_joint_pos[0, list(joint_ids)].detach().cpu().numpy() + return values.astype(np.float32) + + +def _soft_joint_limits(robot: Any, joint_ids: Sequence[int]) -> np.ndarray | None: + limits = as_torch(robot.data.soft_joint_pos_limits) + values = limits[0, list(joint_ids), :].detach().cpu().numpy() + if values.shape != (len(joint_ids), 2): + return None + return values.astype(np.float32) + + +def _parse_args(argv: Sequence[str]) -> argparse.Namespace: + AppLauncher = _load_isaac_lab() + parser = argparse.ArgumentParser( + description="Run a minimal G1 Isaac Lab scene driven by AVP + MANUS teleop." + ) + parser.add_argument("--config", default=str(_default_config_path())) + parser.add_argument("--robot-usd", default=None) + parser.add_argument("--robot-prim", default=None) + parser.add_argument("--duration-s", type=float, default=0.0) + parser.add_argument( + "--robot-height", + type=float, + default=None, + help="Optional override for robot.initial_world_position z.", + ) + parser.add_argument("--light-intensity", type=float, default=3000.0) + parser.add_argument( + "--scene-only", + action="store_true", + help="Load the minimal scene and robot without starting OpenXR/MANUS teleop.", + ) + AppLauncher.add_app_launcher_args(parser) + return parser.parse_args(argv) + + +def main(argv: Sequence[str] | None = None) -> int: + args = _parse_args(sys.argv[1:] if argv is None else argv) + config_path = Path(args.config).expanduser().resolve() + teleop_config = load_yaml_file(config_path) + teleop_config["_config_dir"] = str(config_path.parent) + robot_profile = robot_profile_from_config(teleop_config) + status_label = robot_profile.status_label + head_view_camera_config = _head_view_camera_config(teleop_config) + third_viewpoint_config = _third_viewpoint_config(teleop_config) + initial_world_position = _robot_initial_world_position(teleop_config) + initial_world_orientation_xyzw = _robot_initial_world_orientation_xyzw( + teleop_config + ) + wuji_hand_config = wuji_hand_runtime_config(teleop_config) + arm_ik_config = _arm_ik_runtime_config(teleop_config) + avp_frame_binding_config = avp_frame_binding_runtime_config(teleop_config) + if args.robot_height is not None: + initial_world_position = ( + initial_world_position[0], + initial_world_position[1], + float(args.robot_height), + ) + robot_usd = ( + Path(args.robot_usd).expanduser().resolve() + if args.robot_usd + else example_path(robot_profile.default_usd_relpath) + ) + if not robot_usd.is_file(): + raise FileNotFoundError(f"{status_label} USD not found: {robot_usd}") + robot_prim = str(args.robot_prim or robot_profile.robot_prim) + + AppLauncher = _load_isaac_lab() + app_launcher = AppLauncher(args) + simulation_app = app_launcher.app + + import carb.input + import omni.appwindow + import omni.kit.viewport.utility + import omni.ui + import omni.usd + import torch + from isaacsim.core.rendering_manager import ViewportManager + from pxr import Gf, UsdGeom + + import isaaclab.sim as sim_utils + from isaaclab.sim import SimulationContext + + sim_cfg = sim_utils.SimulationCfg(dt=1.0 / 60.0, device=args.device) + sim = SimulationContext(sim_cfg) + sim.set_camera_view([3.0, -4.0, 2.0], [0.0, 0.0, 0.9]) + render_enabled = not args.headless + + scene_config = G1WujiSceneConfig( + robot_prim=robot_prim, + robot_usd=robot_usd, + initial_world_position=initial_world_position, + initial_world_orientation_xyzw=initial_world_orientation_xyzw, + initial_hand_joint_positions=wuji_hand_config.initial_joint_positions, + light_intensity=args.light_intensity, + ) + robot = design_g1_wuji_scene(scene_config) + + sim.reset() + sim_dt = sim.get_physics_dt() + left_joint_ids = _find_required_joint_ids( + robot, + wuji_hand_config.left_joint_names, + "left", + status_label=status_label, + ) + right_joint_ids = _find_required_joint_ids( + robot, + wuji_hand_config.right_joint_names, + "right", + status_label=status_label, + ) + left_initial_targets = _default_joint_positions( + robot, + left_joint_ids, + ) + right_initial_targets = _default_joint_positions( + robot, + right_joint_ids, + ) + left_postprocessor = HandTargetPostProcessor( + joint_names=wuji_hand_config.left_joint_names, + initial_targets=left_initial_targets, + joint_limits=_soft_joint_limits(robot, left_joint_ids), + config=wuji_hand_config, + side="left", + ) + right_postprocessor = HandTargetPostProcessor( + joint_names=wuji_hand_config.right_joint_names, + initial_targets=right_initial_targets, + joint_limits=_soft_joint_limits(robot, right_joint_ids), + config=wuji_hand_config, + side="right", + ) + wuji_backend = WujiHandTargetBackend(wuji_hand_config) + avp_frame_binding = ( + AvpRobotFrameBinding( + robot, + arm_ik_config, + avp_frame_binding_config, + pose_scale=arm_ik_config.pose_scale, + pose_z_offset=arm_ik_config.pose_z_offset, + ) + if avp_frame_binding_config.enabled + else None + ) + arm_ik = ( + DualArmIkController( + robot, + arm_ik_config, + device=sim.device, + pose_binding=avp_frame_binding, + ) + if arm_ik_config.enabled + else None + ) + stage = omni.usd.get_context().get_stage() + head_link_prim = None + if head_view_camera_config.enabled: + head_link_prim = stage.GetPrimAtPath( + head_view_camera_config.head_link_prim_path + ) + if head_link_prim is None or not head_link_prim.IsValid(): + raise FileNotFoundError( + f"{status_label} head camera prim does not exist: " + f"{head_view_camera_config.head_link_prim_path}" + ) + print( + f"[{status_label}] head camera uses prim {head_link_prim.GetPath()}", + flush=True, + ) + xform_cache = UsdGeom.XformCache() + viewport_api = omni.kit.viewport.utility.get_active_viewport() + default_viewport_camera_path = ( + str(viewport_api.camera_path) + if viewport_api is not None and viewport_api.camera_path is not None + else None + ) + head_view_camera = ( + HeadViewCameraController( + stage=stage, + UsdGeom=UsdGeom, + Gf=Gf, + viewport_api=viewport_api, + config=head_view_camera_config, + head_link_prim=head_link_prim, + avp_frame_binding=avp_frame_binding, + ) + if head_view_camera_config.enabled and stage is not None + else None + ) + third_viewpoint_reference_prim = None + if third_viewpoint_config is not None and stage is not None: + third_viewpoint_reference_path = ( + f"{robot_prim.rstrip('/')}/{avp_frame_binding_config.robot_reference_body}" + ) + third_viewpoint_reference_prim = stage.GetPrimAtPath( + third_viewpoint_reference_path + ) + if ( + third_viewpoint_reference_prim is None + or not third_viewpoint_reference_prim.IsValid() + ): + raise FileNotFoundError( + f"{status_label} third viewpoint reference prim does not exist: " + f"{third_viewpoint_reference_path}" + ) + third_viewpoint = ( + ThirdViewpointController( + Gf=Gf, + ViewportManager=ViewportManager, + viewport_utility=omni.kit.viewport.utility, + ui=omni.ui, + config=third_viewpoint_config, + reference_prim=third_viewpoint_reference_prim, + main_viewport_api=viewport_api, + main_camera_path=( + None if head_view_camera is None else head_view_camera.camera_path + ), + status_label=status_label, + ) + if ( + third_viewpoint_config is not None + and stage is not None + and not args.headless + ) + else None + ) + if third_viewpoint is None: + _hide_third_viewpoint_window(omni.kit.viewport.utility) + teleop: TeleopMain | None = None + teleop_started = False + scene_warmed_up = False + robot_default_joint_pos = as_torch(robot.data.default_joint_pos).clone() + robot_default_joint_vel = as_torch(robot.data.default_joint_vel).clone() + robot.set_joint_position_target( + target=torch.as_tensor(left_initial_targets, device=sim.device).unsqueeze(0), + joint_ids=left_joint_ids, + ) + robot.set_joint_position_target( + target=torch.as_tensor(right_initial_targets, device=sim.device).unsqueeze(0), + joint_ids=right_joint_ids, + ) + + print( + f"{status_label} teleop scene ready: " + f"{len(left_joint_ids)} left hand joints, {len(right_joint_ids)} right hand joints, " + f"hand_retarget={wuji_hand_config.retarget_backend}, " + f"arm_ik={arm_ik_config.enabled}, " + f"avp_frame_binding={avp_frame_binding_config.enabled}, " + f"head_view_camera={head_view_camera is not None}, " + f"third_viewpoint={third_viewpoint is not None}." + ) + + def step_scene_once() -> None: + robot.write_data_to_sim() + sim.step(render=render_enabled) + robot.update(sim_dt) + if head_view_camera is not None: + head_view_camera.update(xform_cache=xform_cache) + + if args.scene_only: + print("Scene-only mode: OpenXR/MANUS teleop is not started.") + start_time = time.monotonic() + while simulation_app.is_running(): + if ( + args.duration_s > 0.0 + and time.monotonic() - start_time >= args.duration_s + ): + break + step_scene_once() + simulation_app.close() + return 0 + + teleop_armed = False + start_waiting_announced = False + keyboard_start_requested = False + keyboard_reset_requested = False + + app_window = omni.appwindow.get_default_app_window() + input_interface = carb.input.acquire_input_interface() + + def on_keyboard_event(event, *args, **kwargs): + nonlocal keyboard_reset_requested, keyboard_start_requested + if event.type == carb.input.KeyboardEventType.KEY_PRESS: + if event.input == carb.input.KeyboardInput.B: + keyboard_start_requested = True + elif event.input == carb.input.KeyboardInput.R: + keyboard_reset_requested = True + return True + + keyboard_subscription = ( + input_interface.subscribe_to_keyboard_events( + app_window.get_keyboard(), + on_keyboard_event, + ) + if app_window is not None and app_window.get_keyboard() is not None + else None + ) + + def hold_robot_targets() -> None: + robot.set_joint_position_target( + target=torch.as_tensor(left_initial_targets, device=sim.device).unsqueeze( + 0 + ), + joint_ids=left_joint_ids, + ) + robot.set_joint_position_target( + target=torch.as_tensor(right_initial_targets, device=sim.device).unsqueeze( + 0 + ), + joint_ids=right_joint_ids, + ) + + def restore_robot_initial_pose() -> None: + robot.write_joint_state_to_sim( + robot_default_joint_pos, + robot_default_joint_vel, + ) + robot.set_joint_position_target( + target=robot_default_joint_pos, + ) + if arm_ik is not None: + arm_ik._last_targets.clear() + + def reset_teleop_calibration() -> None: + if avp_frame_binding is not None: + avp_frame_binding.reset_calibration() + if arm_ik is not None: + arm_ik.reset_startup_references() + if head_view_camera is not None: + head_view_camera.deactivate() + if viewport_api is not None and default_viewport_camera_path: + viewport_api.camera_path = default_viewport_camera_path + if third_viewpoint is not None: + third_viewpoint.deactivate() + + def close_openxr_runtime() -> None: + nonlocal teleop, teleop_started + if teleop_started and teleop is not None: + teleop.__exit__(None, None, None) + teleop = None + teleop_started = False + + def start_openxr_runtime() -> None: + nonlocal teleop, teleop_started + teleop = TeleopMain(teleop_config, config_dir=config_path.parent) + teleop.__enter__() + teleop_started = True + print( + f"[{status_label}] OpenXR teleop session started.", + flush=True, + ) + + start_time = time.monotonic() + if not simulation_app.is_running(): + has_keyboard = bool( + app_window is not None and app_window.get_keyboard() is not None + ) + print( + f"[{status_label}] simulation app is not running at teleop loop start; " + "exiting before the first teleop frame. " + f"headless={args.headless} " + f"app_window={app_window is not None} " + f"keyboard={has_keyboard}", + flush=True, + ) + while simulation_app.is_running(): + if args.duration_s > 0.0 and time.monotonic() - start_time >= args.duration_s: + break + + start_button_rising = keyboard_start_requested + keyboard_start_requested = False + reset_button_rising = keyboard_reset_requested + keyboard_reset_requested = False + + if reset_button_rising: + teleop_armed = False + start_waiting_announced = False + restore_robot_initial_pose() + reset_teleop_calibration() + print( + f"[{status_label}] teleop reset; robot returned to its initial joint pose. " + "Press keyboard B to recalibrate and start again.", + flush=True, + ) + + if not scene_warmed_up: + hold_robot_targets() + step_scene_once() + scene_warmed_up = True + continue + + if teleop is None: + start_openxr_runtime() + + sample = teleop.step() + if not teleop_armed: + if not start_waiting_announced: + print( + f"[{status_label}] teleop is disarmed; press keyboard B to recalibrate " + "and start teleop. Press keyboard R to force reset back to this state.", + flush=True, + ) + start_waiting_announced = True + if start_button_rising: + teleop_armed = True + start_waiting_announced = False + reset_teleop_calibration() + print( + f"[{status_label}] teleop armed at frame={int(sample.get('frame_count', 0))}; " + "starting fresh AVP head-hand calibration " + f"(averaging {avp_frame_binding_config.calibration_window_s:.1f}s).", + flush=True, + ) + else: + hold_robot_targets() + step_scene_once() + continue + + raw_left_targets, raw_right_targets = wuji_backend.targets(sample) + + left_targets = left_postprocessor.process(raw_left_targets) + right_targets = right_postprocessor.process(raw_right_targets) + + if left_targets is not None: + robot.set_joint_position_target( + target=torch.as_tensor(left_targets, device=sim.device).unsqueeze(0), + joint_ids=left_joint_ids, + ) + if right_targets is not None: + robot.set_joint_position_target( + target=torch.as_tensor(right_targets, device=sim.device).unsqueeze(0), + joint_ids=right_joint_ids, + ) + if avp_frame_binding is not None: + avp_frame_binding.update_calibration(sample) + avp_frame_binding.update_runtime_state(sample) + if ( + head_view_camera is not None + and head_view_camera_config.activate_on_calibration + and avp_frame_binding.calibrated + ): + if third_viewpoint is not None: + third_viewpoint.activate(xform_cache=xform_cache) + head_view_camera.activate() + if arm_ik is not None: + arm_ik.update(sample) + + step_scene_once() + + if keyboard_subscription is not None: + input_interface.unsubscribe_from_keyboard_events( + app_window.get_keyboard(), + keyboard_subscription, + ) + close_openxr_runtime() + simulation_app.close() + + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/g1_wuji_teleop/python/g1_wuji_teleop/cli.py b/examples/g1_wuji_teleop/python/g1_wuji_teleop/cli.py new file mode 100644 index 000000000..d994da3b1 --- /dev/null +++ b/examples/g1_wuji_teleop/python/g1_wuji_teleop/cli.py @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Thin CLI entrypoint for the G1-Wuji teleop example.""" + +from __future__ import annotations + +from collections.abc import Sequence + +from .app import main as app_main + + +def teleop_main(argv: Sequence[str] | None = None) -> int: + return app_main(argv) diff --git a/examples/g1_wuji_teleop/python/g1_wuji_teleop/config.py b/examples/g1_wuji_teleop/python/g1_wuji_teleop/config.py new file mode 100644 index 000000000..156731225 --- /dev/null +++ b/examples/g1_wuji_teleop/python/g1_wuji_teleop/config.py @@ -0,0 +1,47 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Configuration and path helpers for the G1-Wuji teleop example.""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any + + +def package_root() -> Path: + return Path(__file__).resolve().parent + + +def example_root() -> Path: + return package_root().parents[1] + + +def default_config_path() -> Path: + return (example_root() / "config" / "avp_manus.yaml").resolve() + + +def default_robot_usd_path() -> Path: + return (example_root() / "assets" / "g1_wuji" / "g1_wuji.usd").resolve() + + +def example_path(relative_path: str | Path) -> Path: + path = Path(relative_path).expanduser() + if path.is_absolute(): + return path.resolve() + return (example_root() / path).resolve() + + +def load_yaml_file(path: Path) -> dict[str, Any]: + try: + import yaml + except ModuleNotFoundError as exc: + raise ModuleNotFoundError( + "PyYAML is required for G1-Wuji teleop configs." + ) from exc + + with path.open("r", encoding="utf-8") as stream: + data = yaml.safe_load(stream) or {} + if not isinstance(data, dict): + raise ValueError(f"Config root must be a mapping: {path}") + return data diff --git a/examples/g1_wuji_teleop/python/g1_wuji_teleop/input_stream.py b/examples/g1_wuji_teleop/python/g1_wuji_teleop/input_stream.py new file mode 100644 index 000000000..3aff60b80 --- /dev/null +++ b/examples/g1_wuji_teleop/python/g1_wuji_teleop/input_stream.py @@ -0,0 +1,506 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""OpenXR + MANUS input stream for the G1-Wuji teleop example.""" + +from __future__ import annotations + +import argparse +import sys +import time +from collections.abc import Mapping, Sequence +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +from .config import default_config_path, load_yaml_file +from .transforms import transform_controller_pose_to_palm + + +SIDE_NAMES = ("left", "right") + + +@dataclass(frozen=True) +class RuntimeDeps: + DeviceIOSession: Any + ControllersSource: Any + HeadSource: Any + HandsSource: Any + OutputCombiner: Any + PluginConfig: Any + TeleopSession: Any + TeleopSessionConfig: Any + ControllerInputIndex: Any + HandInputIndex: Any + HandJointIndex: Any + HeadPoseIndex: Any + + +@dataclass(frozen=True) +class ControllerPalmOffset: + pos: tuple[float, float, float] + quat_xyzw: tuple[float, float, float, float] + + +@dataclass(frozen=True) +class EePoseConfig: + source: str + controller_pose_source: str + hand_joint: str + controller_to_palm: dict[str, ControllerPalmOffset] + + +def _load_runtime_deps() -> RuntimeDeps: + try: + import isaacteleop.deviceio as deviceio + from isaacteleop.retargeting_engine.deviceio_source_nodes import ( + ControllersSource, + HandsSource, + HeadSource, + ) + from isaacteleop.retargeting_engine.interface import OutputCombiner + from isaacteleop.retargeting_engine.tensor_types.indices import ( + ControllerInputIndex, + HandInputIndex, + HandJointIndex, + HeadPoseIndex, + ) + from isaacteleop.teleop_session_manager import ( + PluginConfig, + TeleopSession, + TeleopSessionConfig, + ) + except ModuleNotFoundError as exc: + raise ModuleNotFoundError( + f"Missing Python dependency {exc.name!r}. Activate an IsaacTeleop " + "environment with CloudXR and retargeter dependencies." + ) from exc + + return RuntimeDeps( + DeviceIOSession=deviceio.DeviceIOSession, + ControllersSource=ControllersSource, + HeadSource=HeadSource, + HandsSource=HandsSource, + OutputCombiner=OutputCombiner, + PluginConfig=PluginConfig, + TeleopSession=TeleopSession, + TeleopSessionConfig=TeleopSessionConfig, + ControllerInputIndex=ControllerInputIndex, + HandInputIndex=HandInputIndex, + HandJointIndex=HandJointIndex, + HeadPoseIndex=HeadPoseIndex, + ) + + +def _as_mapping(value: Any, name: str) -> Mapping[str, Any]: + if value is None: + return {} + if not isinstance(value, Mapping): + raise ValueError(f"Config field {name!r} must be a mapping") + return value + + +def _as_sequence(value: Any, name: str) -> Sequence[Any]: + if isinstance(value, (str, bytes)) or not isinstance(value, Sequence): + raise ValueError(f"Config field {name!r} must be a sequence") + return value + + +def _float_tuple(value: Any, *, name: str, length: int) -> tuple[float, ...]: + raw = _as_sequence(value, name) + if len(raw) != length: + raise ValueError(f"Config field {name!r} must contain {length} values") + return tuple(float(item) for item in raw) + + +def _resolve_path(path_value: Any, *, base_dir: Path, name: str) -> Path: + if path_value is None: + raise ValueError(f"Config field {name!r} is required") + path = Path(str(path_value)).expanduser() + if not path.is_absolute(): + path = base_dir / path + return path.resolve() + + +def _load_controller_palm_offsets( + avp_cfg: Mapping[str, Any], +) -> dict[str, ControllerPalmOffset]: + offsets: dict[str, ControllerPalmOffset] = {} + for side in SIDE_NAMES: + offsets[side] = ControllerPalmOffset( + pos=_float_tuple( + avp_cfg.get(f"{side}_controller_to_palm_pos"), + name=f"avp.{side}_controller_to_palm_pos", + length=3, + ), + quat_xyzw=_float_tuple( + avp_cfg.get(f"{side}_controller_to_palm_quat_xyzw"), + name=f"avp.{side}_controller_to_palm_quat_xyzw", + length=4, + ), + ) + return offsets + + +def _load_ee_pose_config(config: Mapping[str, Any]) -> EePoseConfig: + avp_cfg = _as_mapping(config.get("avp"), "avp") + source = str(avp_cfg.get("ee_pose_source", "hand_tracking")).lower() + hand_joint = str(avp_cfg.get("ee_hand_joint", "wrist")).lower() + controller_pose_source = str(avp_cfg.get("controller_pose_source", "grip")).lower() + + if source not in {"controller", "hand_tracking"}: + raise ValueError("avp.ee_pose_source must be 'controller' or 'hand_tracking'") + if hand_joint not in {"wrist", "palm"}: + raise ValueError("avp.ee_hand_joint must be 'wrist' or 'palm'") + if controller_pose_source not in {"aim", "grip"}: + raise ValueError("avp.controller_pose_source must be 'aim' or 'grip'") + + return EePoseConfig( + source=source, + controller_pose_source=controller_pose_source, + hand_joint=hand_joint, + controller_to_palm=( + _load_controller_palm_offsets(avp_cfg) if source == "controller" else {} + ), + ) + + +def _controller_pose( + group: Any, + *, + pose_source: str, + deps: RuntimeDeps, +) -> dict[str, Any] | None: + if group.is_none: + return None + + if pose_source == "aim": + position_index = deps.ControllerInputIndex.AIM_POSITION + orientation_index = deps.ControllerInputIndex.AIM_ORIENTATION + valid_index = deps.ControllerInputIndex.AIM_IS_VALID + elif pose_source == "grip": + position_index = deps.ControllerInputIndex.GRIP_POSITION + orientation_index = deps.ControllerInputIndex.GRIP_ORIENTATION + valid_index = deps.ControllerInputIndex.GRIP_IS_VALID + else: + raise ValueError(f"Unsupported controller pose source: {pose_source!r}") + + if not bool(group[valid_index]): + return None + + return { + "position": [float(value) for value in group[position_index]], + "orientation_xyzw": [float(value) for value in group[orientation_index]], + } + + +def _hand_tracking_pose( + group: Any, + *, + joint_name: str, + deps: RuntimeDeps, +) -> dict[str, Any] | None: + if group.is_none: + return None + + import numpy as np + + joint_index = int(getattr(deps.HandJointIndex, joint_name.upper())) + valid = np.from_dlpack(group[deps.HandInputIndex.JOINT_VALID]) + if not bool(valid[joint_index]): + return None + + positions = np.from_dlpack(group[deps.HandInputIndex.JOINT_POSITIONS]) + orientations = np.from_dlpack(group[deps.HandInputIndex.JOINT_ORIENTATIONS]) + return { + "position": [float(value) for value in positions[joint_index]], + "orientation_xyzw": [float(value) for value in orientations[joint_index]], + } + + +def _head_pose(group: Any, *, deps: RuntimeDeps) -> dict[str, Any] | None: + if group.is_none or not bool(group[deps.HeadPoseIndex.IS_VALID]): + return None + + return { + "position": [float(value) for value in group[deps.HeadPoseIndex.POSITION]], + "orientation_xyzw": [ + float(value) for value in group[deps.HeadPoseIndex.ORIENTATION] + ], + } + + +def _controller_scalar( + group: Any, + index_enum: Any, + field_name: str, +) -> float: + return float(group[getattr(index_enum, field_name)]) + + +def _controller_inputs(group: Any, *, deps: RuntimeDeps) -> dict[str, Any] | None: + if group.is_none: + return None + controller_index = deps.ControllerInputIndex + return { + "primary_click": _controller_scalar(group, controller_index, "PRIMARY_CLICK") + > 0.5, + "secondary_click": _controller_scalar( + group, controller_index, "SECONDARY_CLICK" + ) + > 0.5, + "thumbstick_click": _controller_scalar( + group, controller_index, "THUMBSTICK_CLICK" + ) + > 0.5, + "menu_click": _controller_scalar(group, controller_index, "MENU_CLICK") > 0.5, + "thumbstick_x": _controller_scalar(group, controller_index, "THUMBSTICK_X"), + "thumbstick_y": _controller_scalar(group, controller_index, "THUMBSTICK_Y"), + "squeeze_value": _controller_scalar(group, controller_index, "SQUEEZE_VALUE"), + "trigger_value": _controller_scalar(group, controller_index, "TRIGGER_VALUE"), + } + + +def _openxr_hand_joint_names(deps: RuntimeDeps) -> list[str]: + return [ + name.lower() + for name, _index in sorted( + deps.HandJointIndex.__members__.items(), + key=lambda item: int(item[1]), + ) + ] + + +def _hand_tracking_skeleton(group: Any, *, deps: RuntimeDeps) -> dict[str, Any] | None: + if group.is_none: + return None + + import numpy as np + + positions = np.from_dlpack(group[deps.HandInputIndex.JOINT_POSITIONS]).copy() + valid = np.from_dlpack(group[deps.HandInputIndex.JOINT_VALID]).astype(bool).copy() + if positions.ndim != 2 or positions.shape[1] != 3: + return None + if valid.ndim != 1 or positions.shape[0] != valid.shape[0]: + return None + + return { + "joint_names": _openxr_hand_joint_names(deps), + "positions": [[float(coord) for coord in position] for position in positions], + "valid": [bool(value) for value in valid], + } + + +class TeleopMain: + """Facade returning EE poses and MANUS skeletons from TeleopSession.""" + + def __init__( + self, + config: Mapping[str, Any], + *, + config_dir: Path, + oxr_handles: Any | None = None, + ) -> None: + self._config = config + self._config_dir = config_dir + self._deps = _load_runtime_deps() + self._oxr_handles = oxr_handles + self._session = None + self._session_context = None + self._ee_config = _load_ee_pose_config(config) + self._manus_plugin_configs = self._build_plugin_configs() + self._required_xr_extensions: tuple[str, ...] = () + self._session_config = self._build_session_config() + + @classmethod + def from_config_file( + cls, + path: str | Path | None = None, + *, + oxr_handles: Any | None = None, + ) -> "TeleopMain": + config_path = Path(path).expanduser() if path else default_config_path() + config_path = config_path.resolve() + return cls( + load_yaml_file(config_path), + config_dir=config_path.parent, + oxr_handles=oxr_handles, + ) + + @property + def required_xr_extensions(self) -> tuple[str, ...]: + return tuple(self._required_xr_extensions) + + def set_oxr_handles(self, oxr_handles: Any | None) -> None: + if self._session is not None or self._session_context is not None: + raise RuntimeError( + "TeleopMain.set_oxr_handles() must be called before starting the session" + ) + self._oxr_handles = oxr_handles + self._session_config = self._build_session_config() + + def __enter__(self) -> "TeleopMain": + self._session_context = self._deps.TeleopSession(self._session_config) + self._session = self._session_context.__enter__() + return self + + def __exit__(self, exc_type, exc_value, traceback) -> None: + if self._session_context is not None: + self._session_context.__exit__(exc_type, exc_value, traceback) + self._session_context = None + self._session = None + + def step(self) -> dict[str, Any]: + if self._session is None: + raise RuntimeError("TeleopMain must be entered before step()") + + result = self._session.step() + left_hand = result["openxr_hand_left"] + right_hand = result["openxr_hand_right"] + controller_outputs = self._controller_outputs(result) + return { + "timestamp_s": time.time(), + "frame_count": self._session.frame_count, + "head_pose": _head_pose(result["avp_head"], deps=self._deps), + "ee_poses": { + "left": self._ee_pose(result, "left"), + "right": self._ee_pose(result, "right"), + }, + "hand_active": { + "left": not left_hand.is_none, + "right": not right_hand.is_none, + }, + "hand_skeletons": { + "left": _hand_tracking_skeleton(left_hand, deps=self._deps), + "right": _hand_tracking_skeleton(right_hand, deps=self._deps), + }, + "hand_joint_names": {side: [] for side in SIDE_NAMES}, + "hand_joint_positions": {side: None for side in SIDE_NAMES}, + "controller_inputs": controller_outputs, + } + + def _build_session_config(self): + deps = self._deps + head = deps.HeadSource(name="avp_head") + hands = deps.HandsSource(name="manus_hands") + trackers = [head.get_tracker(), hands.get_tracker()] + output_mapping = { + "avp_head": head.output("head"), + "openxr_hand_left": hands.output(deps.HandsSource.LEFT), + "openxr_hand_right": hands.output(deps.HandsSource.RIGHT), + } + + if self._ee_config.source == "controller": + controllers = deps.ControllersSource(name="avp_controllers") + trackers.append(controllers.get_tracker()) + output_mapping.update( + { + "avp_controller_left": controllers.output( + deps.ControllersSource.LEFT + ), + "avp_controller_right": controllers.output( + deps.ControllersSource.RIGHT + ), + } + ) + + pipeline = deps.OutputCombiner(output_mapping) + self._required_xr_extensions = tuple( + deps.DeviceIOSession.get_required_extensions(trackers) + ) + session_cfg = _as_mapping(self._config.get("session"), "session") + return deps.TeleopSessionConfig( + app_name=str(session_cfg.get("app_name", "G1WujiTeleop")), + pipeline=pipeline, + plugins=self._manus_plugin_configs, + oxr_handles=self._oxr_handles, + ) + + def _build_plugin_configs(self) -> list[Any]: + plugin_cfg = _as_mapping(self._config.get("manus_plugin"), "manus_plugin") + if not bool(plugin_cfg.get("enabled", True)): + return [] + + search_paths_raw = _as_sequence( + plugin_cfg.get("search_paths"), "manus_plugin.search_paths" + ) + search_paths = [ + _resolve_path( + path, base_dir=self._config_dir, name="manus_plugin.search_paths" + ) + for path in search_paths_raw + ] + missing = [path for path in search_paths if not path.is_dir()] + if missing: + raise FileNotFoundError( + "MANUS plugin search path does not exist: " + + ", ".join(str(path) for path in missing) + ) + + return [ + self._deps.PluginConfig( + plugin_name=str(plugin_cfg.get("plugin_name", "manus_hand_plugin")), + plugin_root_id=str(plugin_cfg.get("plugin_root_id", "manus")), + search_paths=search_paths, + enabled=True, + plugin_args=[ + str(arg) + for arg in _as_sequence( + plugin_cfg.get("args", []), "manus_plugin.args" + ) + ], + ) + ] + + def _controller_outputs(self, result: Mapping[str, Any]) -> dict[str, Any]: + if self._ee_config.source != "controller": + return {side: None for side in SIDE_NAMES} + return { + side: _controller_inputs(result[f"avp_controller_{side}"], deps=self._deps) + for side in SIDE_NAMES + } + + def _ee_pose(self, result: Mapping[str, Any], side: str) -> dict[str, Any] | None: + if self._ee_config.source == "hand_tracking": + return _hand_tracking_pose( + result[f"openxr_hand_{side}"], + joint_name=self._ee_config.hand_joint, + deps=self._deps, + ) + + pose = _controller_pose( + result[f"avp_controller_{side}"], + pose_source=self._ee_config.controller_pose_source, + deps=self._deps, + ) + if pose is None: + return None + offset = self._ee_config.controller_to_palm[side] + palm_pos, palm_quat = transform_controller_pose_to_palm( + controller_pos=pose["position"], + controller_quat_xyzw=pose["orientation_xyzw"], + offset_pos=offset.pos, + offset_quat_xyzw=offset.quat_xyzw, + ) + return { + "position": [float(value) for value in palm_pos], + "orientation_xyzw": [float(value) for value in palm_quat], + } + + +def _parse_args(argv: Sequence[str]) -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Run the G1-Wuji input stream.") + parser.add_argument("--config", default=str(default_config_path())) + return parser.parse_args(argv) + + +def main(argv: Sequence[str] | None = None) -> int: + args = _parse_args(sys.argv[1:] if argv is None else argv) + with TeleopMain.from_config_file(args.config) as teleop: + while True: + teleop.step() + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/examples/g1_wuji_teleop/python/g1_wuji_teleop/retargeting.py b/examples/g1_wuji_teleop/python/g1_wuji_teleop/retargeting.py new file mode 100644 index 000000000..8ed6a3679 --- /dev/null +++ b/examples/g1_wuji_teleop/python/g1_wuji_teleop/retargeting.py @@ -0,0 +1,198 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Adapter for the external official ``wuji-retargeting`` package.""" + +from __future__ import annotations + +import importlib.util +import time +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any + +import numpy as np + + +@dataclass(frozen=True) +class HandRetargetStepResult: + frame_index: int + left_joint_positions: np.ndarray + right_joint_positions: np.ndarray + left_joint_names: tuple[str, ...] | None + right_joint_names: tuple[str, ...] | None + ok: bool + error: str | None = None + latency_ms: float = 0.0 + raw: dict[str, Any] = field(default_factory=dict) + + +def import_official_wuji_retargeter(): + if importlib.util.find_spec("wuji_retargeting.retarget") is None: + raise ModuleNotFoundError( + "Official wuji-retargeting is not installed in this Python environment. " + "Install it with `python -m pip install -e /path/to/wuji-retargeting` " + "before running the G1-Wuji teleop example." + ) + + from wuji_retargeting.retarget import Retargeter + + return Retargeter + + +def _as_float32_numpy(values: Any, shape: tuple[int, ...]) -> np.ndarray: + return np.asarray(values, dtype=np.float32).reshape(shape) + + +def _valid_skeleton21(skeleton: np.ndarray) -> tuple[bool, str | None]: + if skeleton.shape != (21, 3): + return False, f"unexpected shape {skeleton.shape}" + if not np.all(np.isfinite(skeleton)): + return False, "non-finite landmarks" + + centered = skeleton - skeleton[0:1] + if float(np.max(np.linalg.norm(centered, axis=1))) < 1.0e-4: + return False, "all landmarks collapsed to the wrist" + + palm_vec_a = skeleton[5] - skeleton[0] + palm_vec_b = skeleton[9] - skeleton[0] + if np.linalg.norm(palm_vec_a) < 1.0e-5 or np.linalg.norm(palm_vec_b) < 1.0e-5: + return False, "missing palm anchor landmarks" + if np.linalg.norm(np.cross(palm_vec_a, palm_vec_b)) < 1.0e-7: + return False, "degenerate palm frame" + return True, None + + +class WujiOfficialManusHandRetargeter: + """Retarget MANUS/OpenXR-style 21-point skeletons with Wuji's package.""" + + def __init__( + self, + *, + config_dir: Path, + left_config_name: str = "left.yaml", + right_config_name: str = "right.yaml", + ) -> None: + self._config_dir = Path(config_dir).expanduser().resolve() + self._config_names = {"left": left_config_name, "right": right_config_name} + self._retargeter_left: Any | None = None + self._retargeter_right: Any | None = None + self._left_joint_names: tuple[str, ...] | None = None + self._right_joint_names: tuple[str, ...] | None = None + + @property + def left_joint_names(self) -> tuple[str, ...] | None: + return self._left_joint_names + + @property + def right_joint_names(self) -> tuple[str, ...] | None: + return self._right_joint_names + + def _resolve_config_path(self, side: str) -> Path: + return (self._config_dir / self._config_names[side]).resolve() + + def _ensure_retargeters(self) -> None: + if self._retargeter_left is not None and self._retargeter_right is not None: + return + + Retargeter = import_official_wuji_retargeter() + left_yaml = self._resolve_config_path("left") + right_yaml = self._resolve_config_path("right") + for config_path in (left_yaml, right_yaml): + if not config_path.is_file(): + raise FileNotFoundError( + f"Wuji retarget config does not exist: {config_path}" + ) + + self._retargeter_left = Retargeter.from_yaml(str(left_yaml), hand_side="left") + self._retargeter_right = Retargeter.from_yaml( + str(right_yaml), hand_side="right" + ) + self._left_joint_names = tuple( + self._retargeter_left.optimizer.robot.dof_joint_names + ) + self._right_joint_names = tuple( + self._retargeter_right.optimizer.robot.dof_joint_names + ) + + @staticmethod + def _retarget_hand(side: str, skeleton: np.ndarray, retargeter: Any) -> np.ndarray: + valid, error = _valid_skeleton21(skeleton) + if not valid: + raise ValueError(f"Invalid {side} hand skeleton: {error}") + + qpos = np.asarray(retargeter.retarget(skeleton), dtype=np.float32).reshape(-1) + if qpos.shape != (int(retargeter.num_joints),): + raise RuntimeError( + f"{side} hand retarget returned {qpos.shape}, expected {(int(retargeter.num_joints),)}" + ) + if not np.all(np.isfinite(qpos)): + raise RuntimeError(f"{side} hand retarget returned non-finite values") + return qpos.copy() + + def retarget_manus_skeletons( + self, + left_skeleton: Any, + right_skeleton: Any, + *, + frame_index: int, + device: Any = None, + ) -> HandRetargetStepResult: + del device + self._ensure_retargeters() + started = time.perf_counter() + + left_qpos = self._retarget_hand( + "left", + _as_float32_numpy(left_skeleton, (21, 3)), + self._retargeter_left, + ) + right_qpos = self._retarget_hand( + "right", + _as_float32_numpy(right_skeleton, (21, 3)), + self._retargeter_right, + ) + + return HandRetargetStepResult( + frame_index=int(frame_index), + left_joint_positions=left_qpos, + right_joint_positions=right_qpos, + left_joint_names=self._left_joint_names, + right_joint_names=self._right_joint_names, + ok=True, + latency_ms=(time.perf_counter() - started) * 1000.0, + ) + + def retarget_manus_skeleton( + self, + side: str, + skeleton: Any, + *, + frame_index: int, + device: Any = None, + ) -> tuple[np.ndarray, tuple[str, ...] | None]: + del frame_index, device + self._ensure_retargeters() + if side == "left": + return ( + self._retarget_hand( + "left", + _as_float32_numpy(skeleton, (21, 3)), + self._retargeter_left, + ), + self._left_joint_names, + ) + if side == "right": + return ( + self._retarget_hand( + "right", + _as_float32_numpy(skeleton, (21, 3)), + self._retargeter_right, + ), + self._right_joint_names, + ) + raise ValueError(f"Unsupported hand side: {side!r}") + + def shutdown(self) -> None: + self._retargeter_left = None + self._retargeter_right = None diff --git a/examples/g1_wuji_teleop/python/g1_wuji_teleop/robot.py b/examples/g1_wuji_teleop/python/g1_wuji_teleop/robot.py new file mode 100644 index 000000000..98da94329 --- /dev/null +++ b/examples/g1_wuji_teleop/python/g1_wuji_teleop/robot.py @@ -0,0 +1,1232 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""MANUS and AVP data helpers for the G1-Wuji teleop example.""" + +from __future__ import annotations + +import time +from collections.abc import Mapping, Sequence +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any + +import numpy as np + +from .config import example_root + +LEFT_WUJI_JOINTS = ( + "left_finger1_joint1", + "left_finger1_joint2", + "left_finger1_joint3", + "left_finger1_joint4", + "left_finger2_joint1", + "left_finger2_joint2", + "left_finger2_joint3", + "left_finger2_joint4", + "left_finger3_joint1", + "left_finger3_joint2", + "left_finger3_joint3", + "left_finger3_joint4", + "left_finger4_joint1", + "left_finger4_joint2", + "left_finger4_joint3", + "left_finger4_joint4", + "left_finger5_joint1", + "left_finger5_joint2", + "left_finger5_joint3", + "left_finger5_joint4", +) + +RIGHT_WUJI_JOINTS = tuple( + name.replace("left_", "right_", 1) for name in LEFT_WUJI_JOINTS +) + +WUJI_SKELETON21_OPENXR_NAMES = ( + "wrist", + "thumb_metacarpal", + "thumb_proximal", + "thumb_distal", + "thumb_tip", + "index_proximal", + "index_intermediate", + "index_distal", + "index_tip", + "middle_proximal", + "middle_intermediate", + "middle_distal", + "middle_tip", + "ring_proximal", + "ring_intermediate", + "ring_distal", + "ring_tip", + "little_proximal", + "little_intermediate", + "little_distal", + "little_tip", +) + +FIXED_WUJI_HAND_CONFIG_DIR = "wuji_retargeting" + + +def _default_wuji_initial_joint_positions() -> dict[str, float]: + return { + "left_finger1_joint1": 0.059, + "left_finger1_joint2": 0.059, + "left_finger1_joint3": 0.059, + "left_finger1_joint4": 0.0, + "left_finger2_joint1": 0.0, + "left_finger2_joint2": 0.0, + "left_finger2_joint3": 0.0, + "left_finger2_joint4": 0.0, + "left_finger3_joint1": 0.0, + "left_finger3_joint2": 0.0, + "left_finger3_joint3": 0.0, + "left_finger3_joint4": 0.0, + "left_finger4_joint1": 0.0, + "left_finger4_joint2": 0.0, + "left_finger4_joint3": 0.0, + "left_finger4_joint4": 0.0, + "left_finger5_joint1": 0.0, + "left_finger5_joint2": 0.0, + "left_finger5_joint3": 0.0, + "left_finger5_joint4": 0.0, + "right_finger1_joint1": 0.037, + "right_finger1_joint2": 0.037, + "right_finger1_joint3": 0.037, + "right_finger1_joint4": 0.0, + "right_finger2_joint1": 0.0, + "right_finger2_joint2": 0.0, + "right_finger2_joint3": 0.0, + "right_finger2_joint4": 0.0, + "right_finger3_joint1": 0.0, + "right_finger3_joint2": 0.0, + "right_finger3_joint3": 0.0, + "right_finger3_joint4": 0.0, + "right_finger4_joint1": 0.0, + "right_finger4_joint2": 0.0, + "right_finger4_joint3": 0.0, + "right_finger4_joint4": 0.0, + "right_finger5_joint1": 0.0, + "right_finger5_joint2": 0.0, + "right_finger5_joint3": 0.0, + "right_finger5_joint4": 0.0, + } + + +@dataclass(frozen=True) +class RobotProfile: + variant: str + robot_prim: str + default_usd_relpath: str + left_hand_joint_names: tuple[str, ...] + right_hand_joint_names: tuple[str, ...] + initial_hand_joint_positions: dict[str, float] + hand_retarget_backend: str + hand_wuji_config_dir: str | None + ik_body_names: dict[str, str] + ik_body_offset_pos: dict[str, tuple[float, float, float]] + ik_body_offset_quat_xyzw: dict[str, tuple[float, float, float, float]] + ik_target_orientation_correction_quat_xyzw: dict[ + str, tuple[float, float, float, float] + ] + reference_body: str + reference_offset_pos: tuple[float, float, float] + reference_offset_quat_xyzw: tuple[float, float, float, float] + axis_signs: dict[str, float] + status_label: str + + +ROBOT_PROFILES: dict[str, RobotProfile] = { + "g1_wuji": RobotProfile( + variant="g1_wuji", + robot_prim="/World/G1Wuji", + default_usd_relpath="assets/g1_wuji/g1_wuji.usd", + left_hand_joint_names=LEFT_WUJI_JOINTS, + right_hand_joint_names=RIGHT_WUJI_JOINTS, + initial_hand_joint_positions=_default_wuji_initial_joint_positions(), + hand_retarget_backend="official_wuji", + hand_wuji_config_dir=FIXED_WUJI_HAND_CONFIG_DIR, + ik_body_names={ + "left": "left_wrist_yaw_link", + "right": "right_wrist_yaw_link", + }, + ik_body_offset_pos={ + "left": (0.0415, 0.003, 0.0), + "right": (0.0415, -0.003, 0.0), + }, + ik_body_offset_quat_xyzw={ + "left": (0.0, 0.0, 0.0, 1.0), + "right": (0.0, 0.0, 0.0, 1.0), + }, + ik_target_orientation_correction_quat_xyzw={ + "left": (0.5, 0.5, 0.5, 0.5), + "right": (-0.5, 0.5, 0.5, -0.5), + }, + reference_body="torso_link", + reference_offset_pos=(0.0077774, 0.0000210, 0.3836842), + reference_offset_quat_xyzw=(0.0, 0.0, 0.0, 1.0), + axis_signs={}, + status_label="G1-Wuji", + ), +} + + +def normalize_robot_variant(value: Any) -> str: + raw = str(value or "g1_wuji").strip().lower() + if raw not in ROBOT_PROFILES: + raise ValueError( + f"Unsupported robot.variant={value!r}. Expected one of {sorted(ROBOT_PROFILES)}" + ) + return raw + + +def robot_profile_from_config(config: Mapping[str, Any]) -> RobotProfile: + robot_config = config.get("robot", {}) + if robot_config is None: + robot_config = {} + if not isinstance(robot_config, Mapping): + raise ValueError("Config field 'robot' must be a mapping") + return ROBOT_PROFILES[normalize_robot_variant(robot_config.get("variant"))] + + +@dataclass(frozen=True) +class WujiHandRuntimeConfig: + robot_variant: str = "g1_wuji" + neutral_calibration_samples: int = 30 + initial_joint_position: float = 0.0 + initial_joint_positions: dict[str, float] = field( + default_factory=_default_wuji_initial_joint_positions + ) + left_joint_names: tuple[str, ...] = LEFT_WUJI_JOINTS + right_joint_names: tuple[str, ...] = RIGHT_WUJI_JOINTS + axis_signs: dict[str, float] = field(default_factory=dict) + retarget_backend: str = "official_wuji" + status_label: str = "teleop" + wuji_config_dir: Path | None = None + retarget_output_mode: str = "absolute" + clamp_to_joint_limits: bool = True + smoothing_alpha: float = 0.8 + + +@dataclass(frozen=True) +class AvpFrameBindingRuntimeConfig: + status_label: str = "teleop" + enabled: bool = True + calibration_window_s: float = 3.0 + robot_reference_body: str = "torso_link" + robot_reference_offset_pos: tuple[float, float, float] = (0.0, 0.0, 0.0) + robot_reference_offset_quat_xyzw: tuple[float, float, float, float] = ( + 0.0, + 0.0, + 0.0, + 1.0, + ) + head_forward_axis_isaac: tuple[float, float, float] = (0.0, 1.0, 0.0) + + +@dataclass +class _AvpHeadHandBindingState: + origin_pos: np.ndarray + frame_rot: np.ndarray + head_rot: np.ndarray + + +@dataclass +class _AvpPendingHeadHandCalibration: + start_timestamp_s: float + last_timestamp_s: float + sample_count: int = 0 + head_pos_sum: np.ndarray = field( + default_factory=lambda: np.zeros(3, dtype=np.float64) + ) + head_rot_sum: np.ndarray = field( + default_factory=lambda: np.zeros((3, 3), dtype=np.float64) + ) + left_pos_sum: np.ndarray = field( + default_factory=lambda: np.zeros(3, dtype=np.float64) + ) + right_pos_sum: np.ndarray = field( + default_factory=lambda: np.zeros(3, dtype=np.float64) + ) + + def append( + self, + *, + timestamp_s: float, + head_pos: np.ndarray, + head_rot: np.ndarray, + left_pos: np.ndarray, + right_pos: np.ndarray, + ) -> None: + self.last_timestamp_s = float(timestamp_s) + self.sample_count += 1 + self.head_pos_sum += np.asarray(head_pos, dtype=np.float64) + self.head_rot_sum += np.asarray(head_rot, dtype=np.float64) + self.left_pos_sum += np.asarray(left_pos, dtype=np.float64) + self.right_pos_sum += np.asarray(right_pos, dtype=np.float64) + + +class HandTargetPostProcessor: + """Map MANUS retarget qpos into stable USD joint position targets.""" + + def __init__( + self, + *, + joint_names: Sequence[str], + initial_targets: np.ndarray, + joint_limits: np.ndarray | None, + config: WujiHandRuntimeConfig, + side: str, + ) -> None: + self._joint_names = tuple(joint_names) + self._initial_targets = np.asarray(initial_targets, dtype=np.float32) + self._joint_limits = ( + None if joint_limits is None else np.asarray(joint_limits, dtype=np.float32) + ) + self._config = config + self._side = side + self._neutral_samples: list[np.ndarray] = [] + self._neutral: np.ndarray | None = None + self._last_target: np.ndarray | None = None + self._announced = False + + @property + def ready(self) -> bool: + return ( + self._config.neutral_calibration_samples <= 0 or self._neutral is not None + ) + + def process(self, raw_targets: np.ndarray | None) -> np.ndarray | None: + if raw_targets is None: + return None + + raw = np.asarray(raw_targets, dtype=np.float32) + if raw.shape != self._initial_targets.shape: + raise ValueError( + f"{self._side} {self._config.status_label} target shape mismatch: expected " + f"{self._initial_targets.shape}, got {raw.shape}" + ) + + if self._config.retarget_output_mode == "absolute": + target = raw + elif self._config.neutral_calibration_samples > 0 and self._neutral is None: + self._neutral_samples.append(raw.copy()) + if len(self._neutral_samples) >= self._config.neutral_calibration_samples: + self._neutral = np.mean(np.stack(self._neutral_samples), axis=0).astype( + np.float32 + ) + print( + f"[{self._config.status_label}] {self._side} neutral calibrated with " + f"{len(self._neutral_samples)} sample(s)." + ) + if self._last_target is not None: + return self._last_target + return self._initial_targets.copy() + + else: + neutral = self._neutral if self._neutral is not None else np.zeros_like(raw) + target = self._initial_targets + (raw - neutral) + + if self._config.clamp_to_joint_limits and self._joint_limits is not None: + target = np.clip(target, self._joint_limits[:, 0], self._joint_limits[:, 1]) + + if self._last_target is not None and self._config.smoothing_alpha < 1.0: + alpha = self._config.smoothing_alpha + target = self._last_target * (1.0 - alpha) + target * alpha + + self._last_target = target.astype(np.float32) + if not self._announced: + print( + f"[{self._config.status_label}] {self._side} hand postprocess active: " + f"mode={self._config.retarget_output_mode}, " + f"neutral_samples={self._config.neutral_calibration_samples}, " + f"clamp={self._config.clamp_to_joint_limits}, " + f"smoothing_alpha={self._config.smoothing_alpha}." + ) + self._announced = True + return self._last_target + + +def wuji_hand_runtime_config(config: Mapping[str, Any]) -> WujiHandRuntimeConfig: + profile = robot_profile_from_config(config) + wuji_config_dir = None + if profile.hand_wuji_config_dir is not None: + wuji_config_dir = _resolve_config_relative_path( + profile.hand_wuji_config_dir, config + ) + if wuji_config_dir is None or not wuji_config_dir.is_dir(): + raise FileNotFoundError( + f"Fixed {profile.status_label} Wuji retarget config directory does not exist: " + f"{wuji_config_dir}" + ) + + return WujiHandRuntimeConfig( + robot_variant=profile.variant, + initial_joint_positions=dict(profile.initial_hand_joint_positions), + left_joint_names=profile.left_hand_joint_names, + right_joint_names=profile.right_hand_joint_names, + axis_signs=dict(profile.axis_signs), + retarget_backend=profile.hand_retarget_backend, + status_label=profile.status_label, + wuji_config_dir=wuji_config_dir, + ) + + +def avp_frame_binding_runtime_config( + config: Mapping[str, Any], +) -> AvpFrameBindingRuntimeConfig: + profile = robot_profile_from_config(config) + robot_config = config.get("robot", {}) + if robot_config is None: + robot_config = {} + if not isinstance(robot_config, Mapping): + raise ValueError("Config field 'robot' must be a mapping") + + binding_config = robot_config.get("avp_frame_binding", {}) + if binding_config is None: + binding_config = {} + if not isinstance(binding_config, Mapping): + raise ValueError("Config field 'robot.avp_frame_binding' must be a mapping") + calibration_window_s = float(binding_config.get("calibration_window_s", 3.0)) + if calibration_window_s < 0.0: + raise ValueError( + "Config field 'robot.avp_frame_binding.calibration_window_s' must be >= 0" + ) + + return AvpFrameBindingRuntimeConfig( + status_label=profile.status_label, + enabled=bool(binding_config.get("enabled", True)), + calibration_window_s=calibration_window_s, + robot_reference_body=str( + binding_config.get("robot_reference_body", profile.reference_body) + ), + robot_reference_offset_pos=_float_tuple( + binding_config.get("robot_reference_offset_pos"), + name="robot.avp_frame_binding.robot_reference_offset_pos", + length=3, + default=profile.reference_offset_pos, + ), + robot_reference_offset_quat_xyzw=_float_tuple( + binding_config.get("robot_reference_offset_quat_xyzw"), + name="robot.avp_frame_binding.robot_reference_offset_quat_xyzw", + length=4, + default=profile.reference_offset_quat_xyzw, + ), + head_forward_axis_isaac=_float_tuple( + binding_config.get("head_forward_axis_isaac"), + name="robot.avp_frame_binding.head_forward_axis_isaac", + length=3, + default=(0.0, 1.0, 0.0), + ), + ) + + +def openxr_to_isaac_position( + position: Sequence[float], scale: float, offset_z: float +) -> np.ndarray: + x, y, z = (float(v) for v in position) + position_isaac = np.array([x, -z, y], dtype=np.float32) * float(scale) + return position_isaac + np.array([0.0, 0.0, offset_z], dtype=np.float32) + + +def quat_xyzw_to_matrix(quat: Sequence[float]) -> np.ndarray: + x, y, z, w = (float(v) for v in quat) + norm = (x * x + y * y + z * z + w * w) ** 0.5 + if norm < 1.0e-8: + return np.eye(3, dtype=np.float64) + x /= norm + y /= norm + z /= norm + w /= norm + return np.array( + [ + [1.0 - 2.0 * (y * y + z * z), 2.0 * (x * y - z * w), 2.0 * (x * z + y * w)], + [2.0 * (x * y + z * w), 1.0 - 2.0 * (x * x + z * z), 2.0 * (y * z - x * w)], + [2.0 * (x * z - y * w), 2.0 * (y * z + x * w), 1.0 - 2.0 * (x * x + y * y)], + ], + dtype=np.float64, + ) + + +def matrix_to_quat_xyzw(matrix: np.ndarray) -> np.ndarray: + m = np.asarray(matrix, dtype=np.float64) + trace = float(np.trace(m)) + if trace > 0.0: + s = (trace + 1.0) ** 0.5 * 2.0 + w = 0.25 * s + x = (m[2, 1] - m[1, 2]) / s + y = (m[0, 2] - m[2, 0]) / s + z = (m[1, 0] - m[0, 1]) / s + elif m[0, 0] > m[1, 1] and m[0, 0] > m[2, 2]: + s = (1.0 + m[0, 0] - m[1, 1] - m[2, 2]) ** 0.5 * 2.0 + w = (m[2, 1] - m[1, 2]) / s + x = 0.25 * s + y = (m[0, 1] + m[1, 0]) / s + z = (m[0, 2] + m[2, 0]) / s + elif m[1, 1] > m[2, 2]: + s = (1.0 + m[1, 1] - m[0, 0] - m[2, 2]) ** 0.5 * 2.0 + w = (m[0, 2] - m[2, 0]) / s + x = (m[0, 1] + m[1, 0]) / s + y = 0.25 * s + z = (m[1, 2] + m[2, 1]) / s + else: + s = (1.0 + m[2, 2] - m[0, 0] - m[1, 1]) ** 0.5 * 2.0 + w = (m[1, 0] - m[0, 1]) / s + x = (m[0, 2] + m[2, 0]) / s + y = (m[1, 2] + m[2, 1]) / s + z = 0.25 * s + quat = np.array([x, y, z, w], dtype=np.float32) + quat_norm = np.linalg.norm(quat) + if quat_norm > 1.0e-8: + quat /= quat_norm + return quat + + +def openxr_to_isaac_orientation(orientation_xyzw: Sequence[float]) -> np.ndarray: + return matrix_to_quat_xyzw(openxr_to_isaac_rotation_matrix(orientation_xyzw)) + + +def openxr_to_isaac_rotation_matrix(orientation_xyzw: Sequence[float]) -> np.ndarray: + frame = np.array( + [[1.0, 0.0, 0.0], [0.0, 0.0, -1.0], [0.0, 1.0, 0.0]], + dtype=np.float64, + ) + return frame @ quat_xyzw_to_matrix(orientation_xyzw) @ frame.T + + +def normalize_quat_xyzw(quat: Sequence[float]) -> np.ndarray: + out = np.asarray(quat, dtype=np.float32) + norm = np.linalg.norm(out) + if norm < 1.0e-8: + return np.array([0.0, 0.0, 0.0, 1.0], dtype=np.float32) + return out / norm + + +def openxr_pose_to_isaac_pose( + pose: Mapping[str, Any], + *, + scale: float, + offset_z: float, +) -> tuple[np.ndarray, np.ndarray]: + return ( + openxr_to_isaac_position(pose["position"], scale, offset_z), + openxr_to_isaac_orientation(pose["orientation_xyzw"]), + ) + + +class AvpRobotFrameBinding: + """Bind a startup head-hand AVP frame to the robot reference body.""" + + def __init__( + self, + robot: Any, + arm_ik_config: Any, + config: AvpFrameBindingRuntimeConfig, + *, + pose_scale: float, + pose_z_offset: float, + ) -> None: + _ = arm_ik_config + self._robot = robot + self._config = config + self._pose_scale = float(pose_scale) + self._pose_z_offset = float(pose_z_offset) + self._reference_body_id = _find_single_body_id( + robot, + config.robot_reference_body, + "AVP robot reference body", + ) + self._reference_offset_pos = np.asarray( + config.robot_reference_offset_pos, dtype=np.float64 + ) + self._reference_offset_rot = quat_xyzw_to_matrix( + config.robot_reference_offset_quat_xyzw + ) + self._head_forward_axis = _unit_vector(config.head_forward_axis_isaac) + if self._head_forward_axis is None: + raise ValueError( + "Config field 'robot.avp_frame_binding.head_forward_axis_isaac' " + "must be non-zero" + ) + self._state: _AvpHeadHandBindingState | None = None + self._pending_calibration: _AvpPendingHeadHandCalibration | None = None + self._waiting_announced = False + self._runtime_frame_rot: np.ndarray | None = None + self._latest_head_tilt_delta_rot = np.eye(3, dtype=np.float64) + + def update_calibration(self, sample: Mapping[str, Any]) -> None: + if not self._config.enabled or self._state is not None: + return + + timestamp_s = _sample_timestamp_s(sample) + pending = self._pending_calibration + calibration = self._calibration_inputs(sample) + if pending is None and calibration is None: + self._maybe_log_waiting(sample) + return + + if pending is None: + pending = _AvpPendingHeadHandCalibration( + start_timestamp_s=timestamp_s, + last_timestamp_s=timestamp_s, + ) + self._pending_calibration = pending + print( + f"[{self._config.status_label}] collecting AVP head-hand calibration samples " + f"frame={int(sample.get('frame_count', 0))} " + f"window_s={self._config.calibration_window_s:.2f}.", + flush=True, + ) + + if calibration is not None: + head_pos, head_rot, left_pos, right_pos = calibration + pending.append( + timestamp_s=timestamp_s, + head_pos=head_pos, + head_rot=head_rot, + left_pos=left_pos, + right_pos=right_pos, + ) + + elapsed_s = max(0.0, timestamp_s - pending.start_timestamp_s) + if elapsed_s < self._config.calibration_window_s: + return + + if pending.sample_count <= 0: + self._pending_calibration = None + self._waiting_announced = False + self._maybe_log_waiting( + sample, + reason=( + "calibration window elapsed without valid head/hand samples " + f"elapsed_s={elapsed_s:.3f}" + ), + ) + return + + averaged_head_pos = pending.head_pos_sum / float(pending.sample_count) + averaged_head_rot = _average_rotation_matrix(pending.head_rot_sum) + averaged_left_pos = pending.left_pos_sum / float(pending.sample_count) + averaged_right_pos = pending.right_pos_sum / float(pending.sample_count) + if averaged_head_rot is None: + self._pending_calibration = None + self._waiting_announced = False + self._maybe_log_waiting( + sample, + reason=( + "averaged head rotation was degenerate " + f"samples={pending.sample_count} elapsed_s={elapsed_s:.3f}" + ), + ) + return + + frame_rot = _head_hand_frame_rotation( + head_rot=averaged_head_rot, + left_pos=averaged_left_pos, + right_pos=averaged_right_pos, + head_forward_axis=self._head_forward_axis, + ) + if frame_rot is None: + wrist_delta = averaged_left_pos - averaged_right_pos + self._pending_calibration = None + self._waiting_announced = False + self._maybe_log_waiting( + sample, + reason=( + "degenerate averaged head/hand axes " + f"samples={pending.sample_count} elapsed_s={elapsed_s:.3f} " + f"head={np.round(averaged_head_pos, 4).tolist()} " + f"left={np.round(averaged_left_pos, 4).tolist()} " + f"right={np.round(averaged_right_pos, 4).tolist()} " + f"left_right_norm={float(np.linalg.norm(wrist_delta)):.6f}" + ), + ) + return + + self._state = _AvpHeadHandBindingState( + origin_pos=averaged_head_pos, + frame_rot=frame_rot, + head_rot=averaged_head_rot, + ) + self._runtime_frame_rot = frame_rot.copy() + self._latest_head_yaw_delta_rad = 0.0 + self._latest_head_tilt_delta_rot = np.eye(3, dtype=np.float64) + self._pending_calibration = None + self._announce_calibration( + sample, + averaged_head_pos, + frame_rot, + elapsed_s=elapsed_s, + sample_count=pending.sample_count, + ) + + def reset_calibration(self) -> None: + self._state = None + self._pending_calibration = None + self._waiting_announced = False + self._runtime_frame_rot = None + self._latest_head_tilt_delta_rot = np.eye(3, dtype=np.float64) + print( + f"[{self._config.status_label}] AVP head-hand frame calibration cleared; " + "waiting for fresh calibration.", + flush=True, + ) + + def update_runtime_state(self, sample: Mapping[str, Any]) -> None: + state = self._state + if not self._config.enabled or state is None: + self._runtime_frame_rot = None + self._latest_head_tilt_delta_rot = np.eye(3, dtype=np.float64) + return + + if self._runtime_frame_rot is None: + self._runtime_frame_rot = state.frame_rot + + head_pose = sample.get("head_pose") + if head_pose is None: + return + + current_head_rot = openxr_to_isaac_rotation_matrix( + head_pose["orientation_xyzw"] + ) + head_tilt_delta_rot = _head_tilt_delta_rotation( + calibration_head_rot=state.head_rot, + current_head_rot=current_head_rot, + head_forward_axis=self._head_forward_axis, + ) + if head_tilt_delta_rot is not None: + self._latest_head_tilt_delta_rot = head_tilt_delta_rot + + self._runtime_frame_rot = state.frame_rot + + @property + def calibrated(self) -> bool: + return self._state is not None + + def latest_head_tilt_delta_rot(self) -> np.ndarray: + return np.array(self._latest_head_tilt_delta_rot, copy=True) + + def transform_pose( + self, + side: str, + pose: Mapping[str, Any], + ) -> tuple[np.ndarray, np.ndarray] | None: + raw_pos, raw_quat = openxr_pose_to_isaac_pose( + pose, + scale=self._pose_scale, + offset_z=self._pose_z_offset, + ) + if not self._config.enabled: + return raw_pos, raw_quat + + state = self._state + if state is None: + return None + + frame_rot = self._runtime_frame_rot + if frame_rot is None: + frame_rot = state.frame_rot + raw_rot = quat_xyzw_to_matrix(raw_quat) + local_pos = frame_rot.T @ ( + np.asarray(raw_pos, dtype=np.float64) - state.origin_pos + ) + local_rot = frame_rot.T @ raw_rot + reference_pos, reference_rot = self._current_reference_world_pose() + target_pos = reference_pos + reference_rot @ local_pos + target_rot = reference_rot @ local_rot + return target_pos.astype(np.float32), matrix_to_quat_xyzw(target_rot) + + def _current_reference_world_pose(self) -> tuple[np.ndarray, np.ndarray]: + body_pos, body_rot = _body_pose_np(self._robot, self._reference_body_id) + return ( + body_pos + body_rot @ self._reference_offset_pos, + body_rot @ self._reference_offset_rot, + ) + + def _calibration_inputs( + self, sample: Mapping[str, Any] + ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray] | None: + head_pose = sample.get("head_pose") + ee_poses = sample.get("ee_poses", {}) + left_pose = ee_poses.get("left") if isinstance(ee_poses, Mapping) else None + right_pose = ee_poses.get("right") if isinstance(ee_poses, Mapping) else None + if head_pose is None or left_pose is None or right_pose is None: + return None + + head_pos, head_quat = openxr_pose_to_isaac_pose( + head_pose, + scale=self._pose_scale, + offset_z=self._pose_z_offset, + ) + left_pos, _left_quat = openxr_pose_to_isaac_pose( + left_pose, + scale=self._pose_scale, + offset_z=self._pose_z_offset, + ) + right_pos, _right_quat = openxr_pose_to_isaac_pose( + right_pose, + scale=self._pose_scale, + offset_z=self._pose_z_offset, + ) + return ( + np.asarray(head_pos, dtype=np.float64), + quat_xyzw_to_matrix(head_quat), + np.asarray(left_pos, dtype=np.float64), + np.asarray(right_pos, dtype=np.float64), + ) + + def _maybe_log_waiting( + self, sample: Mapping[str, Any], *, reason: str | None = None + ) -> None: + if self._waiting_announced: + return + frame_count = int(sample.get("frame_count", 0)) + head_ready = sample.get("head_pose") is not None + ee_poses = sample.get("ee_poses", {}) + left_ready = isinstance(ee_poses, Mapping) and ee_poses.get("left") is not None + right_ready = ( + isinstance(ee_poses, Mapping) and ee_poses.get("right") is not None + ) + detail = f" reason={reason}" if reason else "" + print( + f"[{self._config.status_label}] waiting for AVP head-hand frame calibration " + f"frame={frame_count} head={head_ready} left={left_ready} " + f"right={right_ready}{detail}", + flush=True, + ) + self._waiting_announced = True + + def _announce_calibration( + self, + sample: Mapping[str, Any], + origin_pos: np.ndarray, + frame_rot: np.ndarray, + *, + elapsed_s: float, + sample_count: int, + ) -> None: + reference_body_pos, _reference_body_rot = _body_pose_np( + self._robot, self._reference_body_id + ) + reference_pos, _reference_rot = self._current_reference_world_pose() + print( + f"[{self._config.status_label}] AVP head-hand frame calibrated " + f"frame={int(sample.get('frame_count', 0))} " + f"elapsed_s={elapsed_s:.3f} samples={sample_count} " + f"origin_isaac={origin_pos.tolist()} " + f"x={frame_rot[:, 0].tolist()} y={frame_rot[:, 1].tolist()} " + f"z={frame_rot[:, 2].tolist()} " + f"robot_reference_body={self._config.robot_reference_body!r} " + f"robot_reference_body_pos={reference_body_pos.astype(np.float32).tolist()} " + f"robot_reference_pos={reference_pos.astype(np.float32).tolist()}", + flush=True, + ) + + +class WujiHandTargetBackend: + """Official Wuji retargeting from MANUS/OpenXR skeletons.""" + + def __init__(self, config: WujiHandRuntimeConfig) -> None: + self._config = config + self._retargeter: Any | None = None + self._announced = False + self._waiting_announced = False + + def _ensure_retargeter(self) -> None: + if self._retargeter is not None: + return + if self._config.retarget_backend != "official_wuji": + raise ValueError( + f"Unsupported hand retarget backend: {self._config.retarget_backend!r}" + ) + + from .retargeting import WujiOfficialManusHandRetargeter + + self._retargeter = WujiOfficialManusHandRetargeter( + config_dir=self._config.wuji_config_dir + ) + self._retargeter._ensure_retargeters() + if not self._announced: + print( + f"[{self._config.status_label}] using hand retarget backend: " + f"{self._config.retarget_backend} " + f"(left_joints={len(self._retargeter.left_joint_names or ())}, " + f"right_joints={len(self._retargeter.right_joint_names or ())}, " + f"config={self._config.wuji_config_dir})." + ) + self._announced = True + + @staticmethod + def _to_numpy(values: Any) -> np.ndarray: + if hasattr(values, "detach") and hasattr(values, "cpu"): + values = values.detach().cpu().numpy() + return np.asarray(values, dtype=np.float32).reshape(-1) + + def targets( + self, + sample: Mapping[str, Any], + ) -> tuple[np.ndarray | None, np.ndarray | None]: + left_skeleton = _wuji_skeleton21_from_openxr(sample, "left") + right_skeleton = _wuji_skeleton21_from_openxr(sample, "right") + + if left_skeleton is None and right_skeleton is None: + if not self._waiting_announced: + print( + f"[{self._config.status_label}] waiting for MANUS hand skeletons; " + "arm EE teleop remains independent.", + flush=True, + ) + self._waiting_announced = True + return None, None + + self._ensure_retargeter() + self._waiting_announced = False + frame_index = int(sample.get("frame_count", 0)) + left_targets = None + if left_skeleton is not None: + left_positions, left_joint_names = self._retargeter.retarget_manus_skeleton( + "left", + left_skeleton, + frame_index=frame_index, + device=None, + ) + left_targets = _ordered_named_targets( + values=self._to_numpy(left_positions), + source_names=left_joint_names, + target_names=self._config.left_joint_names, + side="left", + label=self._config.retarget_backend, + axis_signs=self._config.axis_signs, + status_label=self._config.status_label, + ) + + right_targets = None + if right_skeleton is not None: + right_positions, right_joint_names = ( + self._retargeter.retarget_manus_skeleton( + "right", + right_skeleton, + frame_index=frame_index, + device=None, + ) + ) + right_targets = _ordered_named_targets( + values=self._to_numpy(right_positions), + source_names=right_joint_names, + target_names=self._config.right_joint_names, + side="right", + label=self._config.retarget_backend, + axis_signs=self._config.axis_signs, + status_label=self._config.status_label, + ) + return left_targets, right_targets + + +def _resolve_config_relative_path(value: Any, config: Mapping[str, Any]) -> Path | None: + if value is None: + return None + raw = str(value).strip() + if not raw: + return None + path = Path(raw).expanduser() + if path.is_absolute(): + return path.resolve() + default_config_dir = example_root() / "config" + base_dir = Path(str(config.get("_config_dir", default_config_dir))).expanduser() + return (base_dir / path).resolve() + + +def _float_tuple( + values: Any, + *, + name: str, + length: int, + default: Sequence[float], +) -> tuple[float, ...]: + raw = default if values is None else values + if not isinstance(raw, Sequence) or isinstance(raw, (str, bytes)): + raise ValueError(f"Config field {name!r} must be a {length}-element sequence") + if len(raw) != length: + raise ValueError(f"Config field {name!r} must contain exactly {length} values") + return tuple(float(value) for value in raw) + + +def _unit_vector(values: Sequence[float] | np.ndarray) -> np.ndarray | None: + vector = np.asarray(values, dtype=np.float64).reshape(-1) + if vector.size != 3: + return None + norm = np.linalg.norm(vector) + if norm < 1.0e-8: + return None + return vector / norm + + +def _sample_timestamp_s(sample: Mapping[str, Any]) -> float: + timestamp = sample.get("timestamp_s") + if timestamp is None: + return time.monotonic() + return float(timestamp) + + +def _average_rotation_matrix(matrix_sum: np.ndarray) -> np.ndarray | None: + matrix = np.asarray(matrix_sum, dtype=np.float64) + if matrix.shape != (3, 3) or np.linalg.norm(matrix) < 1.0e-8: + return None + + u, _s, vt = np.linalg.svd(matrix) + + rotation = u @ vt + if np.linalg.det(rotation) < 0.0: + u[:, -1] *= -1.0 + rotation = u @ vt + return rotation + + +def _head_hand_frame_rotation( + *, + head_rot: np.ndarray, + left_pos: np.ndarray, + right_pos: np.ndarray, + head_forward_axis: np.ndarray, +) -> np.ndarray | None: + x_axis = _unit_vector(head_rot @ head_forward_axis) + if x_axis is None: + return None + + y_axis_raw = _unit_vector(left_pos - right_pos) + if y_axis_raw is None: + return None + else: + z_axis = _unit_vector(np.cross(x_axis, y_axis_raw)) + if z_axis is None: + return None + else: + y_axis = _unit_vector(np.cross(z_axis, x_axis)) + if y_axis is None: + return None + + z_axis = _unit_vector(np.cross(x_axis, y_axis)) + if z_axis is None: + return None + frame_rot = np.column_stack((x_axis, y_axis, z_axis)) + if np.linalg.det(frame_rot) < 0.0: + frame_rot[:, 2] *= -1.0 + return frame_rot + + +def _head_tilt_delta_rotation( + *, + calibration_head_rot: np.ndarray, + current_head_rot: np.ndarray, + head_forward_axis: np.ndarray, +) -> np.ndarray | None: + calibration_tilt = _yaw_removed_rotation( + rotation=calibration_head_rot, + forward_axis=head_forward_axis, + ) + current_tilt = _yaw_removed_rotation( + rotation=current_head_rot, + forward_axis=head_forward_axis, + ) + if calibration_tilt is None or current_tilt is None: + return None + tilt_delta = current_tilt @ calibration_tilt.T + if np.linalg.det(tilt_delta) < 0.0: + return None + return tilt_delta + + +def _yaw_removed_rotation( + *, + rotation: np.ndarray, + forward_axis: np.ndarray, +) -> np.ndarray | None: + yaw_rot = _world_z_yaw_rotation_from_forward( + rotation=rotation, + forward_axis=forward_axis, + ) + if yaw_rot is None: + return None + return yaw_rot.T @ rotation + + +def _world_z_yaw_rotation_from_forward( + *, + rotation: np.ndarray, + forward_axis: np.ndarray, +) -> np.ndarray | None: + forward_local = _unit_vector(forward_axis) + if forward_local is None: + return None + + reference_forward = _unit_vector( + np.asarray([forward_local[0], forward_local[1], 0.0], dtype=np.float64) + ) + current_forward = _unit_vector(rotation @ forward_local) + if reference_forward is None or current_forward is None: + return None + projected_forward = _unit_vector( + np.asarray([current_forward[0], current_forward[1], 0.0], dtype=np.float64) + ) + if projected_forward is None: + return None + + yaw_rad = float( + np.arctan2( + reference_forward[0] * projected_forward[1] + - reference_forward[1] * projected_forward[0], + reference_forward[0] * projected_forward[0] + + reference_forward[1] * projected_forward[1], + ) + ) + cos_yaw = float(np.cos(yaw_rad)) + sin_yaw = float(np.sin(yaw_rad)) + return np.array( + [ + [cos_yaw, -sin_yaw, 0.0], + [sin_yaw, cos_yaw, 0.0], + [0.0, 0.0, 1.0], + ], + dtype=np.float64, + ) + + +def _find_single_body_id(robot: Any, body_name: str, label: str) -> int: + body_ids, body_names = robot.find_bodies(body_name) + if len(body_ids) == 1: + return int(body_ids[0]) + + available = list(getattr(robot.data, "body_names", []) or []) + preview = available[:40] + raise RuntimeError( + f"Expected one {label} match for {body_name!r}; got {len(body_ids)}: " + f"{body_names}. Available body names first entries: {preview}" + ) + + +def as_torch(value: Any) -> Any: + """Return a torch tensor from Isaac Lab buffers or Warp arrays.""" + import torch + + if isinstance(value, torch.Tensor): + return value + import warp as wp + + return wp.to_torch(value) + + +def _first_numpy(value: Any) -> np.ndarray: + if hasattr(value, "detach"): + return value.detach().cpu().numpy() + + return as_torch(value).detach().cpu().numpy() + + +def _body_pose_np(robot: Any, body_id: int) -> tuple[np.ndarray, np.ndarray]: + body_pos = np.asarray( + _first_numpy(robot.data.body_pos_w)[0, body_id], dtype=np.float64 + ) + body_quat = np.asarray( + _first_numpy(robot.data.body_quat_w)[0, body_id], dtype=np.float64 + ) + return body_pos, quat_xyzw_to_matrix(body_quat) + + +def _ordered_named_targets( + *, + values: Any, + source_names: Sequence[str] | None, + target_names: Sequence[str], + side: str, + label: str, + axis_signs: Mapping[str, float] | None = None, + status_label: str = "teleop", +) -> np.ndarray | None: + if values is None: + return None + + values_array = np.asarray(values, dtype=np.float32).reshape(-1) + if source_names is None: + raise RuntimeError( + f"[{status_label}] retarget backend={label} side={side} did not " + "provide joint names" + ) + + values_by_name = { + str(name): float(values_array[i]) + for i, name in enumerate(source_names) + if i < values_array.size + } + resolved_values: dict[str, float] = {} + used_source_names: set[str] = set() + side_prefix = f"{side}_" + for name in target_names: + source_name = name + if source_name not in values_by_name and name.startswith(side_prefix): + source_name = name.removeprefix(side_prefix) + if source_name in values_by_name: + resolved_values[name] = values_by_name[source_name] + used_source_names.add(source_name) + + missing = [name for name in target_names if name not in resolved_values] + if missing: + extra = [name for name in values_by_name if name not in used_source_names] + raise RuntimeError( + f"[{status_label}] retarget joint-name mismatch " + f"backend={label} side={side} missing={missing[:8]} " + f"total_missing={len(missing)} extra={extra[:8]} total_extra={len(extra)}" + ) + + ordered = np.zeros(len(target_names), dtype=np.float32) + sign_map = axis_signs or {} + for i, name in enumerate(target_names): + ordered[i] = resolved_values[name] * sign_map.get(name, 1.0) + return ordered + + +def _wuji_skeleton21_from_openxr( + sample: Mapping[str, Any], side: str +) -> np.ndarray | None: + skeletons = sample.get("hand_skeletons", {}) + skeleton = skeletons.get(side) if isinstance(skeletons, Mapping) else None + if not isinstance(skeleton, Mapping): + return None + + joint_names = skeleton.get("joint_names") + positions = skeleton.get("positions") + if not isinstance(joint_names, Sequence) or positions is None: + return None + + positions_array = np.asarray(positions, dtype=np.float32) + if positions_array.ndim != 2 or positions_array.shape[1] != 3: + return None + + valid_values = skeleton.get("valid") + valid_array = ( + np.ones(positions_array.shape[0], dtype=bool) + if valid_values is None + else np.asarray(valid_values, dtype=bool).reshape(-1) + ) + name_to_index = {str(name): index for index, name in enumerate(joint_names)} + + out = np.zeros((len(WUJI_SKELETON21_OPENXR_NAMES), 3), dtype=np.float32) + missing: list[str] = [] + for out_index, name in enumerate(WUJI_SKELETON21_OPENXR_NAMES): + source_index = name_to_index.get(name) + if ( + source_index is None + or source_index >= positions_array.shape[0] + or source_index >= valid_array.size + or not bool(valid_array[source_index]) + ): + missing.append(name) + continue + out[out_index] = positions_array[source_index] + + if missing: + return None + + if not np.all(np.isfinite(out)): + return None + return out diff --git a/examples/g1_wuji_teleop/python/g1_wuji_teleop/scene.py b/examples/g1_wuji_teleop/python/g1_wuji_teleop/scene.py new file mode 100644 index 000000000..63054a43a --- /dev/null +++ b/examples/g1_wuji_teleop/python/g1_wuji_teleop/scene.py @@ -0,0 +1,291 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Isaac Lab scene helpers for the G1-Wuji teleop app.""" + +from __future__ import annotations + +from collections.abc import Mapping +from dataclasses import dataclass +from pathlib import Path +from typing import Any + + +@dataclass(frozen=True) +class G1WujiSceneConfig: + robot_prim: str + robot_usd: Path + initial_world_position: tuple[float, float, float] + initial_world_orientation_xyzw: tuple[float, float, float, float] + initial_hand_joint_positions: Mapping[str, float] + light_intensity: float + + +def _xyzw_to_wxyz( + quat_xyzw: tuple[float, float, float, float], +) -> tuple[float, float, float, float]: + x, y, z, w = quat_xyzw + return (w, x, y, z) + + +def make_g1_wuji_robot_cfg(config: G1WujiSceneConfig): + import isaaclab.sim as sim_utils + from isaaclab.actuators import ImplicitActuatorCfg + from isaaclab.assets import ArticulationCfg + + joint_pos = { + "right_wrist_yaw_joint": 0.0, + "left_wrist_yaw_joint": 0.0, + ".*_wrist_pitch_joint": 0.0, + ".*_wrist_roll_joint": 0.0, + ".*_shoulder_pitch_joint": 0.0, + ".*_shoulder_roll_joint": 0.0, + ".*_shoulder_yaw_joint": 0.0, + "right_elbow_joint": 0.0, + "left_elbow_joint": 0.0, + } + joint_pos.update( + { + name: float(value) + for name, value in config.initial_hand_joint_positions.items() + } + ) + + return ArticulationCfg( + prim_path=config.robot_prim, + spawn=sim_utils.UsdFileCfg( + usd_path=str(config.robot_usd), + activate_contact_sensors=False, + rigid_props=sim_utils.RigidBodyPropertiesCfg( + disable_gravity=True, + retain_accelerations=False, + max_linear_velocity=100.0, + max_angular_velocity=1000.0, + max_depenetration_velocity=1.0, + ), + collision_props=sim_utils.CollisionPropertiesCfg( + contact_offset=0.002, + rest_offset=0.001, + ), + articulation_props=sim_utils.ArticulationRootPropertiesCfg( + enabled_self_collisions=False, + solver_position_iteration_count=16, + solver_velocity_iteration_count=4, + sleep_threshold=0.005, + stabilization_threshold=0.001, + ), + ), + init_state=ArticulationCfg.InitialStateCfg( + pos=config.initial_world_position, + # Config stores xyzw; IsaacLab InitialStateCfg.rot expects wxyz. + rot=_xyzw_to_wxyz(config.initial_world_orientation_xyzw), + joint_pos=joint_pos, + joint_vel={".*": 0.0}, + ), + soft_joint_pos_limit_factor=0.98, + actuators=_wuji_actuators(ImplicitActuatorCfg), + ) + + +def _wuji_actuators(ImplicitActuatorCfg): + common = { + "base": ImplicitActuatorCfg( + joint_names_expr=["base_.*"], + effort_limit_sim=100000.0, + velocity_limit_sim=1000.0, + stiffness=1e6, + damping=1e4, + ), + "right_arm": ImplicitActuatorCfg( + joint_names_expr=["right_shoulder_.*", "right_elbow_joint"], + effort_limit_sim=5.0, + velocity_limit_sim=3.0, + stiffness=1000.0, + damping=80.0, + ), + "left_arm": ImplicitActuatorCfg( + joint_names_expr=["left_shoulder_.*", "left_elbow_joint"], + effort_limit_sim=5.0, + velocity_limit_sim=3.0, + stiffness=1000.0, + damping=80.0, + ), + "wrist": ImplicitActuatorCfg( + joint_names_expr=[ + "left_wrist_roll_joint", + "left_wrist_pitch_joint", + "left_wrist_yaw_joint", + "right_wrist_roll_joint", + "right_wrist_pitch_joint", + "right_wrist_yaw_joint", + ], + effort_limit_sim=90000.0, + velocity_limit_sim=1200.0, + stiffness=1000.0, + damping=80.0, + ), + } + common["fingers"] = ImplicitActuatorCfg( + joint_names_expr=["left_finger.*", "right_finger.*"], + effort_limit_sim=300.0, + velocity_limit_sim=8.0, + stiffness=3000.0, + damping=120.0, + ) + return common + + +def _make_shape_cfg_without_visual_material(shape_cfg_type: Any, **kwargs: Any) -> Any: + kwargs.pop("visual_material", None) + return shape_cfg_type(**kwargs) + + +def _apply_shape_display_color( + prim_path: str, display_color: tuple[float, float, float] +) -> None: + import omni.usd + from pxr import Gf, UsdGeom + + stage = omni.usd.get_context().get_stage() + if stage is None: + return + mesh_prim = stage.GetPrimAtPath(f"{prim_path}/geometry/mesh") + if mesh_prim is None or not mesh_prim.IsValid(): + return + UsdGeom.Gprim(mesh_prim).CreateDisplayColorAttr( + [Gf.Vec3f(*(float(value) for value in display_color))] + ) + + +def _spawn_display_color_shape( + prim_path: str, + cfg: Any, + *, + display_color: tuple[float, float, float], + **kwargs: Any, +) -> None: + cfg.func(prim_path, cfg, **kwargs) + _apply_shape_display_color(prim_path, display_color) + + +def design_g1_wuji_scene(config: G1WujiSceneConfig) -> Any: + import isaaclab.sim as sim_utils + from isaaclab.assets import Articulation + + graspable_surface_material = sim_utils.RigidBodyMaterialCfg( + static_friction=1.3, + dynamic_friction=1.1, + restitution=0.0, + friction_combine_mode="max", + restitution_combine_mode="min", + ) + + floor_color = (0.25, 0.25, 0.25) + floor_cfg = _make_shape_cfg_without_visual_material( + sim_utils.CuboidCfg, + size=(6.0, 6.0, 0.04), + collision_props=sim_utils.CollisionPropertiesCfg(), + ) + _spawn_display_color_shape( + "/World/Floor", + floor_cfg, + translation=(0.0, 0.0, -0.02), + display_color=floor_color, + ) + + table_color = (0.54, 0.42, 0.30) + table_leg_color = (0.22, 0.22, 0.24) + table_top_size = (1.20, 0.75, 0.05) + table_top_center = (0.0, 0.5, 0.74) + table_leg_size = (0.06, 0.06, 0.72) + table_leg_x_offset = table_top_size[0] * 0.5 - 0.09 + table_leg_y_offset = table_top_size[1] * 0.5 - 0.09 + table_leg_z = table_leg_size[2] * 0.5 + + table_top_cfg = _make_shape_cfg_without_visual_material( + sim_utils.CuboidCfg, + size=table_top_size, + collision_props=sim_utils.CollisionPropertiesCfg(), + physics_material=graspable_surface_material, + ) + _spawn_display_color_shape( + "/World/Props/TableTop", + table_top_cfg, + translation=table_top_center, + display_color=table_color, + ) + + table_leg_cfg = _make_shape_cfg_without_visual_material( + sim_utils.CuboidCfg, + size=table_leg_size, + collision_props=sim_utils.CollisionPropertiesCfg(), + physics_material=graspable_surface_material, + ) + for leg_index, (x_sign, y_sign) in enumerate( + ((1.0, 1.0), (1.0, -1.0), (-1.0, 1.0), (-1.0, -1.0)), start=1 + ): + _spawn_display_color_shape( + f"/World/Props/TableLeg{leg_index}", + table_leg_cfg, + translation=( + table_top_center[0] + x_sign * table_leg_x_offset, + table_top_center[1] + y_sign * table_leg_y_offset, + table_leg_z, + ), + display_color=table_leg_color, + ) + + cylinder_radius = 0.032 + cylinder_height = 0.18 + cylinder_surface_z = table_top_center[2] + table_top_size[2] * 0.5 + cylinder_color = (0.84, 0.18, 0.10) + cylinder_cfg = _make_shape_cfg_without_visual_material( + sim_utils.CylinderCfg, + radius=cylinder_radius, + height=cylinder_height, + axis="Z", + collision_props=sim_utils.CollisionPropertiesCfg( + contact_offset=0.003, + rest_offset=0.0, + ), + rigid_props=sim_utils.RigidBodyPropertiesCfg( + rigid_body_enabled=True, + kinematic_enabled=False, + disable_gravity=False, + linear_damping=0.08, + angular_damping=0.06, + max_linear_velocity=8.0, + max_angular_velocity=50.0, + max_depenetration_velocity=3.0, + solver_position_iteration_count=16, + solver_velocity_iteration_count=4, + sleep_threshold=0.002, + stabilization_threshold=0.001, + ), + mass_props=sim_utils.MassPropertiesCfg(mass=0.18), + physics_material=sim_utils.RigidBodyMaterialCfg( + static_friction=1.4, + dynamic_friction=1.2, + restitution=0.0, + friction_combine_mode="max", + restitution_combine_mode="min", + ), + ) + _spawn_display_color_shape( + "/World/Props/GraspCylinder", + cylinder_cfg, + translation=( + table_top_center[0], + table_top_center[1] - 0.12, + cylinder_surface_z + cylinder_height * 0.5, + ), + display_color=cylinder_color, + ) + + light_cfg = sim_utils.DomeLightCfg( + intensity=config.light_intensity, + color=(0.75, 0.75, 0.75), + ) + light_cfg.func("/World/Light", light_cfg) + + return Articulation(cfg=make_g1_wuji_robot_cfg(config)) diff --git a/examples/g1_wuji_teleop/python/g1_wuji_teleop/transforms.py b/examples/g1_wuji_teleop/python/g1_wuji_teleop/transforms.py new file mode 100644 index 000000000..2560f2f6b --- /dev/null +++ b/examples/g1_wuji_teleop/python/g1_wuji_teleop/transforms.py @@ -0,0 +1,64 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Small transform helpers used by the G1-Wuji teleop example.""" + +from __future__ import annotations + +from collections.abc import Sequence + +import numpy as np + + +def normalize_quat_xyzw(quat: Sequence[float]) -> np.ndarray: + quat_array = np.asarray(quat, dtype=np.float64).reshape(4) + norm = float(np.linalg.norm(quat_array)) + if norm < 1.0e-12: + raise ValueError("Quaternion must be non-zero.") + return quat_array / norm + + +def quat_xyzw_to_matrix(quat: Sequence[float]) -> np.ndarray: + x, y, z, w = normalize_quat_xyzw(quat) + xx, yy, zz = x * x, y * y, z * z + xy, xz, yz = x * y, x * z, y * z + wx, wy, wz = w * x, w * y, w * z + return np.array( + [ + [1.0 - 2.0 * (yy + zz), 2.0 * (xy - wz), 2.0 * (xz + wy)], + [2.0 * (xy + wz), 1.0 - 2.0 * (xx + zz), 2.0 * (yz - wx)], + [2.0 * (xz - wy), 2.0 * (yz + wx), 1.0 - 2.0 * (xx + yy)], + ], + dtype=np.float64, + ) + + +def quat_xyzw_multiply( + lhs: Sequence[float], + rhs: Sequence[float], +) -> np.ndarray: + lx, ly, lz, lw = normalize_quat_xyzw(lhs) + rx, ry, rz, rw = normalize_quat_xyzw(rhs) + return normalize_quat_xyzw( + [ + lw * rx + lx * rw + ly * rz - lz * ry, + lw * ry - lx * rz + ly * rw + lz * rx, + lw * rz + lx * ry - ly * rx + lz * rw, + lw * rw - lx * rx - ly * ry - lz * rz, + ] + ) + + +def transform_controller_pose_to_palm( + *, + controller_pos: Sequence[float], + controller_quat_xyzw: Sequence[float], + offset_pos: Sequence[float], + offset_quat_xyzw: Sequence[float], +) -> tuple[np.ndarray, np.ndarray]: + controller_pos_array = np.asarray(controller_pos, dtype=np.float64).reshape(3) + offset_pos_array = np.asarray(offset_pos, dtype=np.float64).reshape(3) + controller_rot = quat_xyzw_to_matrix(controller_quat_xyzw) + palm_pos = controller_pos_array + controller_rot @ offset_pos_array + palm_quat = quat_xyzw_multiply(controller_quat_xyzw, offset_quat_xyzw) + return palm_pos, palm_quat diff --git a/examples/g1_wuji_teleop/python/pyproject.toml b/examples/g1_wuji_teleop/python/pyproject.toml new file mode 100644 index 000000000..f2bb525ab --- /dev/null +++ b/examples/g1_wuji_teleop/python/pyproject.toml @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +[project] +name = "g1-wuji-teleop-example" +version = "0.0.0" +description = "G1-Wuji AVP/VR + MANUS teleop example" +requires-python = ">=3.10,<3.14" +dependencies = [ + "isaacteleop[cloudxr,retargeters]", + "numpy>=1.23.0", + "PyYAML>=6.0", +]