Skip to content

Add G1 Wuji teleop example - #699

Open
supinelay wants to merge 1 commit into
NVIDIA:mainfrom
LightwheelAI:lightwheel/wuji_teleop
Open

Add G1 Wuji teleop example#699
supinelay wants to merge 1 commit into
NVIDIA:mainfrom
LightwheelAI:lightwheel/wuji_teleop

Conversation

@supinelay

@supinelay supinelay commented Jun 24, 2026

Copy link
Copy Markdown

Description

Add a self-contained G1-Wuji teleoperation example under examples/g1_wuji_teleop.

The example wires AVP + MANUS and VR/PICO + MANUS input into the G1-Wuji robot and Wuji hand
retargeting workflow without modifying the underlying CloudXR, WebXR, MANUS plugin, or Televiz
runtime code.

Changes include:

  • Add examples/g1_wuji_teleop example package and CMake entry.
  • Add AVP + MANUS and VR/PICO + MANUS example configurations.
  • Add the Python example runtime for input streaming, transforms, retargeting, robot control,
    and scene setup.
  • Add G1-Wuji USD asset, Wuji hand URDFs, meshes, and retargeting configs.
  • Add setup and run documentation under examples/g1_wuji_teleop/docs/setup.md.

Fixes: N/A

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Testing

Tested locally on Linux.

Commands run:

PYTHONPATH=examples/g1_wuji_teleop/python \
  python -m compileall -q examples/g1_wuji_teleop/python

git diff --check nvidia/main..HEAD

SKIP=check-copyright-year pre-commit run --all-files

Result: passed.

Notes:

- No C++ files are touched in the current PR version, so clang-format was not applicable.
- This branch does not include examples/g1_wuji_teleop/python_tests, so pytest was not run.
- Runtime hardware validation with AVP/PICO, MANUS, and G1-Wuji hardware will be performed
  separately.

## Checklist

- [x] I have read and understood the CONTRIBUTING.md
- [x] I have run the linter and formatter with SKIP=check-copyright-year pre-commit run --all-
  files

- [x] I have made corresponding changes to the documentation
- [ ] I have added tests that prove my fix/feature works, or explained why not
- [x] I have signed off all my commits (git commit -s) per CONTRIBUTING.md#signing-your-work

@github-actions

Copy link
Copy Markdown
Contributor

📝 Docs preview is not auto-deployed for fork PRs.

A maintainer with write access to NVIDIA/IsaacTeleop can deploy a preview by
commenting /preview-docs on this PR. Once deployed, the preview
will live at:

https://nvidia.github.io/IsaacTeleop/preview/pr-699/

@coderabbitai

coderabbitai Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 2ddbcb61-48d1-4762-b836-69a1b459be5b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR introduces a complete G1-Wuji AVP+MANUS teleoperation example for Isaac Teleop. It adds a new examples/g1_wuji_teleop directory containing: left/right Wuji hand URDF models (5-finger kinematic chains), Git LFS pointers for all STL meshes and USD robot assets, YAML retargeting configs (local DexPilot and official MANUS Wuji), and a Python package implementing TeleopMain (AVP+MANUS session stream), WujiOfficialManusHandRetargeter (upstream wuji-retargeting wrapper), AvpRobotFrameBinding (calibration state machine), G1WujiSceneConfig (Isaac Lab scene), and a standalone 3D visualizer. Supporting changes include an avp_manus.sh runbook, a WebXR AVP controller shim in CloudXRComponent.tsx with debug overlay in App.tsx/CloudXRUI.tsx, a --disable-optical-wrist flag for the MANUS C++ plugin, a QuadLayer Python binding fix using lambdas, and repo config updates (gitattributes, gitignore, gitmodules, REUSE.toml, submodule pin).

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.07% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding a new G1 Wuji teleoperation example.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@avp_manus.sh`:
- Around line 170-172: The final demo launch command uses an invalid config path
that will not resolve from a normal repo checkout. Update the config argument in
the main teleop command in avp_man.sh to use a repo-relative path or the correct
full IsaacTeleop path, and make sure the g1_wuji_teleop_main.py invocation still
points to the avp_manus.yml config correctly.

In `@examples/g1_wuji_teleop/python/g1_wuji_teleop/devices/avp_manus_stream.py`:
- Around line 53-58: The retargeter state path in
FIXED_G1_WUJI_RETARGET_DEFAULTS is hardcoded to a shared /tmp filename, which
can collide across runs and be abused via pre-created symlinks. Update the
left/right parameter_config_path entries in avp_manus_stream.py to generate a
unique file per process in a private runtime directory instead of using fixed
/tmp/...json names, and route the code that consumes these defaults to use the
generated path via the existing default config structure.

In `@examples/g1_wuji_teleop/python/g1_wuji_teleop/paths.py`:
- Around line 32-37: resolve_repo_relative_path currently checks app_root() but
never actually falls back to repo_root(), so repo-relative paths are resolved
from the wrong base. Update this function to first try the app_root()-based path
and, if it does not exist, resolve the same relative_path against repo_root()
instead; keep the behavior centered on resolve_repo_relative_path, app_root(),
and repo_root() so the fallback is clear and correct.

In `@examples/g1_wuji_teleop/python/g1_wuji_teleop/robots/g1_wuji/debug_viz.py`:
- Around line 350-378: The frame marker updater only handles "left" and "right",
so head markers are skipped and never refreshed. Update
update_frame_axis_markers() to also process the "head" key (or otherwise make
the side iteration configurable) so update_head_marker() can pass {"head":
head_pose} and still drive frame_markers.visualize for handles.head_link_frame.

In `@examples/g1_wuji_teleop/python/g1_wuji_teleop/robots/g1_wuji/runtime.py`:
- Around line 1002-1017: The early return in runtime.py’s hand retargeting path
is too strict: the current left_input/right_input check in the logic around
_last_left_skeleton, _last_right_skeleton, and _waiting_announced blocks updates
for one side when only the other side is missing. Change the fallback flow so
each hand is processed independently using its own skeleton or last-known value,
and only skip the side that truly has no usable input instead of returning both
_last_left_targets and _last_right_targets together. Keep the waiting message in
the same retargeting helper, but gate it on both sides being unavailable rather
than one side missing.

In `@examples/g1_wuji_teleop/python/g1_wuji_teleop/viz/teleop_visualizer.py`:
- Line 24: The teleop visualizer is hardcoding a shared MPLCONFIGDIR under /tmp,
which should be replaced with a uniquely created per-user/per-process cache
directory. Update teleop_visualizer.py where
os.environ.setdefault("MPLCONFIGDIR", ...) is set so it points to a freshly
created private temp directory for each run, using a location derived from the
current process/user rather than a fixed /tmp/matplotlib path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: a2eb2022-8fea-477e-9c9e-c6423d590ccb

📥 Commits

Reviewing files that changed from the base of the PR and between 7002ed6 and 25c9ff6.

📒 Files selected for processing (147)
  • .gitattributes
  • .gitignore
  • .gitmodules
  • AGENTS.md
  • REUSE.toml
  • avp_manus.sh
  • deps/cloudxr/webxr_client/helpers/react/CloudXRComponent.tsx
  • deps/cloudxr/webxr_client/src/App.tsx
  • deps/cloudxr/webxr_client/src/CloudXRUI.tsx
  • examples/g1_wuji_teleop/README.md
  • examples/g1_wuji_teleop/assets/g1_wuji/g1_three_fingers.usd
  • examples/g1_wuji_teleop/assets/g1_wuji/g1_wuji.usd
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_link1.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_link1_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_link2.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_link2_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_link3.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_link3_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_link4.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_link4_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_tip_link.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger1_tip_link_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_link1.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_link1_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_link2.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_link2_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_link3.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_link3_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_link4.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_link4_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_tip_link.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger2_tip_link_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_link1.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_link1_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_link2.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_link2_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_link3.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_link3_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_link4.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_link4_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_tip_link.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger3_tip_link_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_link1.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_link1_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_link2.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_link2_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_link3.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_link3_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_link4.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_link4_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_tip_link.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger4_tip_link_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_link1.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_link1_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_link2.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_link2_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_link3.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_link3_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_link4.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_link4_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_tip_link.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/finger5_tip_link_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/palm_link.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/left/palm_link_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_link1.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_link1_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_link2.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_link2_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_link3.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_link3_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_link4.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_link4_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_tip_link.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger1_tip_link_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_link1.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_link1_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_link2.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_link2_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_link3.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_link3_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_link4.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_link4_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_tip_link.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger2_tip_link_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_link1.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_link1_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_link2.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_link2_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_link3.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_link3_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_link4.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_link4_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_tip_link.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger3_tip_link_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_link1.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_link1_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_link2.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_link2_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_link3.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_link3_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_link4.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_link4_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_tip_link.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger4_tip_link_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_link1.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_link1_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_link2.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_link2_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_link3.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_link3_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_link4.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_link4_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_tip_link.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/finger5_tip_link_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/palm_link.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/meshes/right/palm_link_collision.STL
  • examples/g1_wuji_teleop/assets/wuji_hand/urdf/left.urdf
  • examples/g1_wuji_teleop/assets/wuji_hand/urdf/right.urdf
  • examples/g1_wuji_teleop/config/avp_manus.yml
  • examples/g1_wuji_teleop/config/local/g1_wuji/hand_left_config.yml
  • examples/g1_wuji_teleop/config/local/g1_wuji/hand_right_config.yml
  • examples/g1_wuji_teleop/config/official/g1_wuji/manus_wuji_left.yaml
  • examples/g1_wuji_teleop/config/official/g1_wuji/manus_wuji_right.yaml
  • examples/g1_wuji_teleop/python/g1_wuji_teleop/__init__.py
  • examples/g1_wuji_teleop/python/g1_wuji_teleop/cli.py
  • examples/g1_wuji_teleop/python/g1_wuji_teleop/devices/avp_manus_stream.py
  • examples/g1_wuji_teleop/python/g1_wuji_teleop/hand_retargeting/__init__.py
  • examples/g1_wuji_teleop/python/g1_wuji_teleop/hand_retargeting/base.py
  • examples/g1_wuji_teleop/python/g1_wuji_teleop/hand_retargeting/wuji_official_adapter.py
  • examples/g1_wuji_teleop/python/g1_wuji_teleop/paths.py
  • examples/g1_wuji_teleop/python/g1_wuji_teleop/robots/g1_wuji/__init__.py
  • examples/g1_wuji_teleop/python/g1_wuji_teleop/robots/g1_wuji/debug_viz.py
  • examples/g1_wuji_teleop/python/g1_wuji_teleop/robots/g1_wuji/runtime.py
  • examples/g1_wuji_teleop/python/g1_wuji_teleop/robots/g1_wuji/scene.py
  • examples/g1_wuji_teleop/python/g1_wuji_teleop/session.py
  • examples/g1_wuji_teleop/python/g1_wuji_teleop/viz/teleop_visualizer.py
  • examples/g1_wuji_teleop/scripts/g1_wuji_teleop_main.py
  • examples/g1_wuji_teleop/scripts/g1_wuji_teleop_visualizer.py
  • examples/teleop/CMakeLists.txt
  • examples/teleop/python/pyproject.toml
  • scripts/setup_v2d_src.sh
  • src/plugins/manus/app/main.cpp
  • src/plugins/manus/core/inc/manus/manus_hand_tracking_plugin.hpp
  • src/plugins/manus/core/manus_hand_tracking_plugin.cpp
  • src/viz/python/layers_bindings.cpp
  • src/viz/python_tests/test_offscreen_session.py
  • third_party/wuji-retargeting

Comment thread avp_manus.sh Outdated
Comment thread examples/g1_wuji_teleop/python/g1_wuji_teleop/devices/avp_manus_stream.py Outdated
Comment thread examples/g1_wuji_teleop/python/g1_wuji_teleop/paths.py Outdated
Comment thread examples/g1_wuji_teleop/python/g1_wuji_teleop/robots/g1_wuji/debug_viz.py Outdated
Comment thread examples/g1_wuji_teleop/python/g1_wuji_teleop/robots/g1_wuji/runtime.py Outdated
Comment thread examples/g1_wuji_teleop/python/g1_wuji_teleop/viz/teleop_visualizer.py Outdated
@supinelay
supinelay force-pushed the lightwheel/wuji_teleop branch 2 times, most recently from 6047d99 to 99c3146 Compare June 30, 2026 11:42
@supinelay

Copy link
Copy Markdown
Author

Updated the PR to a single example-only commit.

The current diff is limited to CMakeLists.txt and examples/g1_wuji_teleop/**. The previous lower-layer changes to
CloudXR/WebXR, MANUS plugin bindings, Televiz bindings, debug visualizer code, run scripts, and the third_party/wuji- retargeting submodule were removed.

Most CodeRabbit inline comments now refer to files that no longer exist in the current PR version:

  • avp_manus.sh
  • devices/avp_manus_stream.py
  • paths.py
  • robots/g1_wuji/debug_viz.py
  • viz/teleop_visualizer.py

Signed-off-by: lei.hu <lei.hu@git.lightwheel.ai>
@supinelay
supinelay force-pushed the lightwheel/wuji_teleop branch from 99c3146 to 8816f3f Compare July 2, 2026 07:07
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),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please fix: This conversion is inverted against Isaac Lab 3.x, masked by a config value that's wrong the opposite way. InitialStateCfg.rot is documented (x, y, z, w) — the identity default (0,0,0,1) confirms the order — as of current Isaac Lab (checked against 3.0.0 / isaaclab 7.4.0; the field moved to xyzw in the 2.x→3.x migration), so _xyzw_to_wxyz (line 24) reorders it wrongly. It only looks right because initial_world_orientation_xyzw: [0, 1, 0, 0] is itself wrong (180° about +Y, upside-down) and the bad conversion happens to land on the 180° yaw you want. The trap: anyone editing this documented-as-xyzw key with a real xyzw value (e.g. identity [0,0,0,1]) gets a silently rotated robot, no error. Please delete _xyzw_to_wxyz, pass the normalized quat straight to rot=, and set initial_world_orientation_xyzw to [0, 0, 1, 0] in both avp_manus.yaml and vr_manus.yaml.


# G1-Wuji AVP/VR + MANUS Setup

This runbook assumes an Isaac Lab Python environment and an IsaacTeleop build

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please fix: The example's largest dependency, and the runbook only "assumes an Isaac Lab Python environment" — no how-to, no required version, no mention that Isaac Sim (and the omni.* / pxr / torch modules the code imports) comes with it. This isn't just convenience: orientation correctness flips across Isaac Lab's wxyzxyzw migration (see the scene.py note), so the wrong generation gives a subtly wrong robot, not an error. Please (1) add an "Install Isaac Lab" step with a concrete command and minimum version (e.g. pip install "isaaclab[isaacsim,all]" --extra-index-url https://pypi.nvidia.com, or link the official docs), noting every later step runs in that same env; (2) state the supported version in pyproject.toml/README; (3) add a startup assertion (e.g. matrix_from_quat([0,0,0,1]) is identity) so a mismatched env fails loudly.

)


class WujiHandTargetBackend:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please fix: WujiHandTargetBackend (and _wuji_skeleton21_from_openxr below) run retargeting outside the pipeline on a bespoke per-frame dict format built in input_stream.py, rather than as a retargeting-engine node. The canonical pattern for wrapping an external retargeter as a BaseRetargeter via OutputCombiner is src/retargeters/dex_hand_retargeter.py, and src/retargeters/sharpa_hand_retargeter.py already has the OpenXR-26→21 mapping this file re-derives. Since examples teach the integration path, please restructure the hand side as a retargeter node (example-local is fine), leaving only sim-coupled logic (arm IK, frame binding) in the Isaac Lab loop — that should also shed a lot of glue in robot.py.

plugin_name: manus_hand_plugin
plugin_root_id: manus
search_paths:
- ../../../install/plugins

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please fix: search_paths: [../../../install/plugins] is resolved relative to the config file, so it only works from a source tree. In the install tree from this example's CMakeLists.txt (which installs config docs assets under <prefix>/examples/g1_wuji_teleop) it points at <prefix>/install/plugins, which doesn't exist — and input_stream.py hard-fails on a missing search path, so the installed example can't run. Sibling examples anchor on __file__ to work from both trees; please default the search path in code relative to the installed package and keep the YAML key as an optional override. Same in vr_manus.yaml:11.

def start_openxr_runtime() -> None:
nonlocal teleop, teleop_started
teleop = TeleopMain(teleop_config, config_dir=config_path.parent)
teleop.__enter__()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please fix: TeleopMain is entered with a bare __enter__() and only exited on a clean loop finish. Retargeting raises on transient bad input (e.g. an invalid/collapsed skeleton), so one bad glove frame unwinds out of the loop without __exit__, leaking the MANUS/OpenXR session docs/setup.md notes is single-owner — recovery then needs a kill and replug. Please wrap the lifecycle in try/finally (or use TeleopMain as a real with), and consider degrading a transient invalid skeleton to "hold last target" (as you already do for a missing frame) instead of aborting.

)
self._startup_ref_announced.add(side)

def _compute_delta_joint_pos(self, delta_pose: Any, jacobian: Any) -> Any:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please fix: _compute_delta_joint_pos implements pinv/svd/trans solvers (plus matching _ik_params branches and command_type == "position" paths), but _arm_ik_runtime_config hardcodes ik_method="dls" / command_type="pose" and nothing feeds these from config — so ~60 lines can never run, partly re-implementing DifferentialIKController internals. Relatedly, the ArmIkRuntimeConfig dataclass defaults (orientation_error_scale=0.35, smoothing_alpha=0.35, deadbands, enabled, pose_scale/pose_z_offset) are all overridden by different hardcoded values, so the defaults are dead and misleading to anyone tuning. Please keep only the dls/pose path and collapse the config to the fields that vary.

third_viewpoint:
relative_position_m: [0.0, -1.2, 0.55]

avp:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please fix: In the VR/PICO config, this avp: section configures controllers, not an Apple Vision Pro — and the AVP branding runs through the code it maps onto: avp_head / avp_controller_* outputs, the AvpRobotFrameBinding class and its robot.avp_frame_binding key, and an "AVP head-hand frame calibrated" log even in VR mode. The underlying library names these device-agnostically (HeadSource, ControllersSource, HandsSource). YAML keys are a public surface users copy, so better to rename now — e.g. xr_input: / robot.xr_frame_binding / HeadHandFrameBinding and head / controller_* outputs. Genuinely AVP-specific comments can stay.

return rotation


def _head_hand_frame_rotation(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please fix: _head_hand_frame_rotation builds the operator anchor frame from the full 3D head-forward vector, but _world_z_yaw_rotation_from_forward later decomposes yaw about world-Z, which only holds if that frame is horizontal. If the operator looks down during calibration (natural when checking their hands), the frame pitches and operator-vertical leaks into robot-forward. Please project head-forward onto the horizontal plane before building the frame so both paths share one assumption.

name.replace("left_", "right_", 1) for name in LEFT_WUJI_JOINTS
)

WUJI_SKELETON21_OPENXR_NAMES = (

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please fix: This 26→21 remap and the hand-off to wuji-retargeting are the key convention for anyone adapting a different glove or tracker, and it's all implicit — the order is MediaPipe hand-landmark order (wrist; thumb CMC/MCP/IP/TIP; then MCP/PIP/DIP/TIP per finger, index-MCP and middle-MCP at indices 5 and 9), and landmarks go to the retargeter in raw OpenXR world coordinates (Y-up, meters). Please add a short docstring stating order, units, and frame. Also worth noting whether mediapipe_rotation z: ±15° in the retargeting configs is a MANUS sensor offset or frame-convention compensation — that's the first thing that breaks when the input source changes.

return frame @ quat_xyzw_to_matrix(orientation_xyzw) @ frame.T


def normalize_quat_xyzw(quat: Sequence[float]) -> np.ndarray:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please fix: normalize_quat_xyzw (here) and quat_xyzw_to_matrix (line 433) duplicate same-named functions in transforms.py, with different behavior — the transforms.py versions raise on a zero-norm quaternion and return float64; these silently return identity/default and use float32. Same names, opposite failure behavior, imported both ways (input_stream.py from transforms, app.py from robot) — a latent bug. Please keep one copy in transforms.py (I'd keep the raising behavior) and import from there; consolidating the quat math there also gives the package one convention boundary, pairing well with the scene.py fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants