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 new G1 Wuji teleoperation example that integrates AVP + Manus input with the Wuji hand retargeting workflow.

Changes include:

  • Add examples/g1_wuji_teleop example package, configs, scripts, and README.
  • Add Wuji hand assets, URDFs, and G1 Wuji USD assets.
  • Add Wuji official retargeting adapter and related runtime/session code.
  • Add third_party/wuji-retargeting submodule.
  • Update Manus plugin bindings and Televiz Python layer bindings needed by the example.

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:

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

Result: passed.

Also verified touched C++ files with clang-format 14:

clang-format --dry-run --Werror \
  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

Result: passed.

Runtime hardware validation with AVP, Manus, and G1 Wuji hardware was not run as part of this local PR preparation.

## 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 the ../CONTRIBUTING.md#signing-your-work


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **New Features**
* Added a new G1-Wuji teleoperation example with launchers, runtime support, configuration, and visual debugging tools.
* Added support for additional 3D assets and hand models for both left and right hands.
* Expanded WebXR/CloudXR diagnostics with optional on-screen input-source debugging and controller-handling controls.

* **Documentation**
* Added setup and run instructions for the new teleop example and related workflows.

* **Bug Fixes**
* Improved hand-tracking fallback behavior and session compatibility when optical wrist tracking is unavailable.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Signed-off-by: lei.hu <lei.hu@git.lightwheel.ai>
@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

📝 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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
Comment on lines +170 to +172
python examples/g1_wuji_teleop/scripts/g1_wuji_teleop_main.py \
--config /examples/g1_wuji_teleop/config/avp_manus.yml \
--device cuda:0 \

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fix invalid config path in the final demo command.

Line 171 uses an absolute path (/examples/...) that won’t resolve from the repo checkout and will break the main launch step. Use a repo-relative path or the full /path/to/IsaacTeleop/... path.

Suggested fix
 python examples/g1_wuji_teleop/scripts/g1_wuji_teleop_main.py \
-    --config /examples/g1_wuji_teleop/config/avp_manus.yml \
+    --config examples/g1_wuji_teleop/config/avp_manus.yml \
     --device cuda:0 \
     --viz kit
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
python examples/g1_wuji_teleop/scripts/g1_wuji_teleop_main.py \
--config /examples/g1_wuji_teleop/config/avp_manus.yml \
--device cuda:0 \
python examples/g1_wuji_teleop/scripts/g1_wuji_teleop_main.py \
--config examples/g1_wuji_teleop/config/avp_manus.yml \
--device cuda:0 \
🤖 Prompt for 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.

In `@avp_manus.sh` around lines 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.

Comment on lines +53 to +58
FIXED_G1_WUJI_RETARGET_DEFAULTS = {
"left": {
"config": Path("local/g1_wuji/hand_left_config.yml"),
"urdf": Path("../assets/wuji_hand/urdf/left.urdf"),
"parameter_config_path": "/tmp/avp_manus_left_g1_wuji_dex_params.json",
"joint_names": [

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not use fixed filenames under /tmp for retargeter state.

These paths are shared across users/processes, so concurrent runs can stomp each other and a pre-created symlink can redirect writes outside the temp area. Generate a unique file in a private runtime directory instead of hardcoding /tmp/...json.

Also applies to: 82-85

🧰 Tools
🪛 ast-grep (0.44.0)

[info] 56-56: Do not hardcode temporary file or directory names
Context: "/tmp/avp_manus_left_g1_wuji_dex_params.json"
Note: [CWE-377] Insecure Temporary File.

(hardcoded-tmp-file)

🤖 Prompt for 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.

In `@examples/g1_wuji_teleop/python/g1_wuji_teleop/devices/avp_manus_stream.py`
around lines 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.

Source: Linters/SAST tools

Comment on lines +32 to +37
def resolve_repo_relative_path(relative_path: str | Path) -> Path:
relative = Path(relative_path)
app_path = app_root() / relative
if app_path.exists():
return app_path.resolve()
return (app_root() / relative).resolve()

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

resolve_repo_relative_path never falls back to repo root.

Lines 35-37 return the same app_root()-based path in both branches, so repo-relative inputs are never resolved from repo_root().

Proposed fix
 def resolve_repo_relative_path(relative_path: str | Path) -> Path:
     relative = Path(relative_path)
-    app_path = app_root() / relative
-    if app_path.exists():
-        return app_path.resolve()
-    return (app_root() / relative).resolve()
+    app_candidate = (app_root() / relative).resolve()
+    if app_candidate.exists():
+        return app_candidate
+    return (repo_root() / relative).resolve()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def resolve_repo_relative_path(relative_path: str | Path) -> Path:
relative = Path(relative_path)
app_path = app_root() / relative
if app_path.exists():
return app_path.resolve()
return (app_root() / relative).resolve()
def resolve_repo_relative_path(relative_path: str | Path) -> Path:
relative = Path(relative_path)
app_candidate = (app_root() / relative).resolve()
if app_candidate.exists():
return app_candidate
return (repo_root() / relative).resolve()
🤖 Prompt for 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.

In `@examples/g1_wuji_teleop/python/g1_wuji_teleop/paths.py` around lines 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.

Comment on lines +350 to +378
def update_frame_axis_markers(
frame_markers: Any | None,
frame_poses: Mapping[str, FramePose],
axis_length: float,
) -> None:
if frame_markers is None:
return
axis_translations: list[np.ndarray] = []
axis_orientations: list[np.ndarray] = []
axis_indices: list[int] = []
for side in ("left", "right"):
frame_pose = frame_poses.get(side)
if frame_pose is None:
continue

position, rotation = frame_pose
for axis_index in range(3):
axis = rotation[:, axis_index]
axis_translations.append(
position + axis.astype(np.float32) * 0.5 * axis_length
)
axis_orientations.append(_axis_to_cylinder_orientation(axis))
axis_indices.append(axis_index)
if axis_translations:
frame_markers.visualize(
translations=np.stack(axis_translations),
orientations=np.stack(axis_orientations),
marker_indices=np.asarray(axis_indices, dtype=np.int32),
)

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Head frame markers never update.

update_head_marker() calls this helper with {"head": head_pose}, but this loop only reads "left"/"right", so handles.head_link_frame stays at its bootstrap pose.

Suggested fix
-    for side in ("left", "right"):
-        frame_pose = frame_poses.get(side)
-        if frame_pose is None:
-            continue
-
+    for frame_pose in frame_poses.values():
         position, rotation = frame_pose
         for axis_index in range(3):
             axis = rotation[:, axis_index]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def update_frame_axis_markers(
frame_markers: Any | None,
frame_poses: Mapping[str, FramePose],
axis_length: float,
) -> None:
if frame_markers is None:
return
axis_translations: list[np.ndarray] = []
axis_orientations: list[np.ndarray] = []
axis_indices: list[int] = []
for side in ("left", "right"):
frame_pose = frame_poses.get(side)
if frame_pose is None:
continue
position, rotation = frame_pose
for axis_index in range(3):
axis = rotation[:, axis_index]
axis_translations.append(
position + axis.astype(np.float32) * 0.5 * axis_length
)
axis_orientations.append(_axis_to_cylinder_orientation(axis))
axis_indices.append(axis_index)
if axis_translations:
frame_markers.visualize(
translations=np.stack(axis_translations),
orientations=np.stack(axis_orientations),
marker_indices=np.asarray(axis_indices, dtype=np.int32),
)
def update_frame_axis_markers(
frame_markers: Any | None,
frame_poses: Mapping[str, FramePose],
axis_length: float,
) -> None:
if frame_markers is None:
return
axis_translations: list[np.ndarray] = []
axis_orientations: list[np.ndarray] = []
axis_indices: list[int] = []
for frame_pose in frame_poses.values():
position, rotation = frame_pose
for axis_index in range(3):
axis = rotation[:, axis_index]
axis_translations.append(
position + axis.astype(np.float32) * 0.5 * axis_length
)
axis_orientations.append(_axis_to_cylinder_orientation(axis))
axis_indices.append(axis_index)
if axis_translations:
frame_markers.visualize(
translations=np.stack(axis_translations),
orientations=np.stack(axis_orientations),
marker_indices=np.asarray(axis_indices, dtype=np.int32),
)
🤖 Prompt for 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.

In `@examples/g1_wuji_teleop/python/g1_wuji_teleop/robots/g1_wuji/debug_viz.py`
around lines 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.

Comment on lines +1002 to +1017
left_input = (
left_skeleton if left_skeleton is not None else self._last_left_skeleton
)
right_input = (
right_skeleton if right_skeleton is not None else self._last_right_skeleton
)
if left_input is None or right_input is None:
if not self._waiting_announced:
print(
f"[{self._config.status_label}] waiting for MANUS hand skeletons; "
"AVP/controller EE teleop remains independent.",
flush=True,
)
self._waiting_announced = True
return self._last_left_targets, self._last_right_targets

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.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

One missing hand blocks retargeting for the other hand.

This early return waits for both skeletons to exist at least once, so a missing/late right hand prevents left-hand targets from updating at all. That defeats the per-side fallback behavior the adapter already implements.

Suggested fix
-        if left_input is None or right_input is None:
+        if left_input is None and right_input is None:
             if not self._waiting_announced:
                 print(
                     f"[{self._config.status_label}] waiting for MANUS hand skeletons; "
                     "AVP/controller EE teleop remains independent.",
                     flush=True,
                 )
                 self._waiting_announced = True
             return self._last_left_targets, self._last_right_targets
+
+        if left_input is None:
+            left_input = np.zeros((21, 3), dtype=np.float32)
+        if right_input is None:
+            right_input = np.zeros((21, 3), dtype=np.float32)
🤖 Prompt for 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.

In `@examples/g1_wuji_teleop/python/g1_wuji_teleop/robots/g1_wuji/runtime.py`
around lines 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.

from pathlib import Path
from typing import Any

os.environ.setdefault("MPLCONFIGDIR", "/tmp/matplotlib")

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.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Avoid a fixed shared MPLCONFIGDIR under /tmp.

Using /tmp/matplotlib makes every run share the same writable cache/config directory, which can fail under multi-user contention and is susceptible to symlink-based clobbering. Point this at a uniquely created per-user/per-process directory instead.

🤖 Prompt for 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.

In `@examples/g1_wuji_teleop/python/g1_wuji_teleop/viz/teleop_visualizer.py` at
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.

Source: Linters/SAST tools

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant