Timsty commited on
Commit
1f3a93e
·
verified ·
1 Parent(s): c26f879

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. GR00T-WholeBodyControl/decoupled_wbc/__init__.py +3 -0
  2. GR00T-WholeBodyControl/decoupled_wbc/version.py +11 -0
  3. GR00T-WholeBodyControl/docs/Makefile +20 -0
  4. GR00T-WholeBodyControl/docs/README.md +74 -0
  5. GR00T-WholeBodyControl/gear_sonic.egg-info/PKG-INFO +72 -0
  6. GR00T-WholeBodyControl/gear_sonic.egg-info/SOURCES.txt +276 -0
  7. GR00T-WholeBodyControl/gear_sonic.egg-info/dependency_links.txt +1 -0
  8. GR00T-WholeBodyControl/gear_sonic.egg-info/requires.txt +66 -0
  9. GR00T-WholeBodyControl/gear_sonic.egg-info/top_level.txt +2 -0
  10. GR00T-WholeBodyControl/gear_sonic/__init__.py +0 -0
  11. GR00T-WholeBodyControl/gear_sonic/camera/__init__.py +29 -0
  12. GR00T-WholeBodyControl/gear_sonic/camera/composed_camera.py +692 -0
  13. GR00T-WholeBodyControl/gear_sonic/config/base.yaml +52 -0
  14. GR00T-WholeBodyControl/gear_sonic/config/base_eval.yaml +26 -0
  15. GR00T-WholeBodyControl/gear_sonic/config/eval_exp.yaml +34 -0
  16. GR00T-WholeBodyControl/gear_sonic/data/robot_model/__init__.py +5 -0
  17. GR00T-WholeBodyControl/gear_sonic/data/robot_model/__pycache__/__init__.cpython-310.pyc +0 -0
  18. GR00T-WholeBodyControl/gear_sonic/data/robot_model/__pycache__/robot_model.cpython-310.pyc +0 -0
  19. GR00T-WholeBodyControl/gear_sonic/data/robot_model/instantiation/__init__.py +5 -0
  20. GR00T-WholeBodyControl/gear_sonic/data/robot_model/instantiation/__pycache__/__init__.cpython-310.pyc +0 -0
  21. GR00T-WholeBodyControl/gear_sonic/data/robot_model/instantiation/__pycache__/g1.cpython-310.pyc +0 -0
  22. GR00T-WholeBodyControl/gear_sonic/data/robot_model/instantiation/g1.py +62 -0
  23. GR00T-WholeBodyControl/gear_sonic/data/robot_model/model_data/g1/g1_29dof_with_hand.urdf +1497 -0
  24. GR00T-WholeBodyControl/gear_sonic/data/robot_model/model_data/g1/g1_29dof_with_hand.xml +751 -0
  25. GR00T-WholeBodyControl/gear_sonic/data/robot_model/model_data/g1/meshes/left_wrist_pitch_link.STL +0 -0
  26. GR00T-WholeBodyControl/gear_sonic/data/robot_model/model_data/g1/scene_43dof.xml +30 -0
  27. GR00T-WholeBodyControl/gear_sonic/data/robot_model/robot_model.py +817 -0
  28. GR00T-WholeBodyControl/gear_sonic/data/robot_model/supplemental_info/__init__.py +7 -0
  29. GR00T-WholeBodyControl/gear_sonic/data/robot_model/supplemental_info/__pycache__/__init__.cpython-310.pyc +0 -0
  30. GR00T-WholeBodyControl/gear_sonic/data/robot_model/supplemental_info/__pycache__/robot_supplemental_info.cpython-310.pyc +0 -0
  31. GR00T-WholeBodyControl/gear_sonic/data/robot_model/supplemental_info/g1/__init__.py +0 -0
  32. GR00T-WholeBodyControl/gear_sonic/data/robot_model/supplemental_info/g1/__pycache__/__init__.cpython-310.pyc +0 -0
  33. GR00T-WholeBodyControl/gear_sonic/data/robot_model/supplemental_info/g1/__pycache__/g1_supplemental_info.cpython-310.pyc +0 -0
  34. GR00T-WholeBodyControl/gear_sonic/data/robot_model/supplemental_info/g1/g1_supplemental_info.py +334 -0
  35. GR00T-WholeBodyControl/gear_sonic/data/robot_model/supplemental_info/robot_supplemental_info.py +93 -0
  36. GR00T-WholeBodyControl/gear_sonic/eval_agent_trl.py +670 -0
  37. GR00T-WholeBodyControl/gear_sonic/eval_exp.py +679 -0
  38. GR00T-WholeBodyControl/gear_sonic/pyproject.toml +116 -0
  39. GR00T-WholeBodyControl/gear_sonic/scripts/launch_data_collection.py +473 -0
  40. GR00T-WholeBodyControl/gear_sonic/scripts/launch_inference.py +465 -0
  41. GR00T-WholeBodyControl/gear_sonic/scripts/pico_manager_thread_server.py +2311 -0
  42. GR00T-WholeBodyControl/gear_sonic/scripts/process_dataset.py +634 -0
  43. GR00T-WholeBodyControl/gear_sonic/scripts/run_camera_viewer.py +264 -0
  44. GR00T-WholeBodyControl/gear_sonic/scripts/run_data_exporter.py +963 -0
  45. GR00T-WholeBodyControl/gear_sonic/scripts/run_sim_loop.py +68 -0
  46. GR00T-WholeBodyControl/gear_sonic/scripts/run_vla_inference.py +777 -0
  47. GR00T-WholeBodyControl/gear_sonic/scripts/run_vla_inference_dex1_head.py +697 -0
  48. GR00T-WholeBodyControl/gear_sonic/tests/test_dex1_head_inference.py +230 -0
  49. GR00T-WholeBodyControl/gear_sonic/tests/test_input_readers.py +50 -0
  50. GR00T-WholeBodyControl/gear_sonic/train_agent_trl.py +487 -0
GR00T-WholeBodyControl/decoupled_wbc/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .version import VERSION, VERSION_SHORT # noqa
2
+
3
+ __version__ = VERSION # noqa
GR00T-WholeBodyControl/decoupled_wbc/version.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ _MAJOR = "0"
2
+ _MINOR = "1"
3
+ # On main and in a nightly release the patch should be one ahead of the last
4
+ # released build.
5
+ _PATCH = "0"
6
+ # This is mainly for nightly builds which have the suffix ".dev$DATE". See
7
+ # https://semver.org/#is-v123-a-semantic-version for the semantics.
8
+ _SUFFIX = ""
9
+
10
+ VERSION_SHORT = "{0}.{1}".format(_MAJOR, _MINOR)
11
+ VERSION = "0.1.0" # or whatever version you want
GR00T-WholeBodyControl/docs/Makefile ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Minimal makefile for Sphinx documentation
2
+ #
3
+
4
+ # You can set these variables from the command line, and also
5
+ # from the environment for the first two.
6
+ SPHINXOPTS ?=
7
+ SPHINXBUILD ?= sphinx-build
8
+ SOURCEDIR = source
9
+ BUILDDIR = build
10
+
11
+ # Put it first so that "make" without argument is like "make help".
12
+ help:
13
+ @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14
+
15
+ .PHONY: help Makefile
16
+
17
+ # Catch-all target: route all unknown targets to Sphinx using the new
18
+ # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19
+ %: Makefile
20
+ @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
GR00T-WholeBodyControl/docs/README.md ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Documentation
2
+
3
+ This directory contains the source code for the GR00T-WholeBodyControl documentation website.
4
+
5
+ ## Building Locally
6
+
7
+ ### Prerequisites
8
+
9
+ Install the required Python packages:
10
+
11
+ ```bash
12
+ pip install sphinx sphinx-book-theme sphinx-design sphinxemoji \
13
+ autodocsumm sphinxcontrib-bibtex myst-parser \
14
+ sphinx-copybutton
15
+ ```
16
+
17
+ ### Build the Documentation
18
+
19
+ ```bash
20
+ cd docs
21
+ make html
22
+ ```
23
+
24
+ The built documentation will be in `build/html/`. Open `build/html/index.html` in your browser.
25
+
26
+ ### Live Preview
27
+
28
+ Start a local web server to preview:
29
+
30
+ ```bash
31
+ cd build/html
32
+ python -m http.server 8000
33
+ ```
34
+
35
+ Then open http://localhost:8000
36
+
37
+ ### Clean Build
38
+
39
+ To remove all built files and rebuild from scratch:
40
+
41
+ ```bash
42
+ make clean
43
+ make html
44
+ ```
45
+
46
+ ## Deployment
47
+
48
+ The documentation is automatically built and deployed to GitHub Pages when changes are pushed to the `main` branch via the GitHub Actions workflow at `.github/workflows/docs.yml`.
49
+
50
+ The live documentation will be available at:
51
+ **https://nvlabs.github.io/GR00T-WholeBodyControl/**
52
+
53
+ ## Documentation Structure
54
+
55
+ - `source/` - All documentation source files
56
+ - `conf.py` - Sphinx configuration
57
+ - `index.rst` - Main landing page
58
+ - `_static/` - Static assets (CSS, images, logos)
59
+ - `tutorials/` - Tutorial pages
60
+ - `getting_started/` - Getting started guides
61
+ - `user_guide/` - User guide
62
+ - `api/` - API reference
63
+ - `resources/` - Additional resources
64
+
65
+ ## Writing Documentation
66
+
67
+ - Use Markdown (`.md`) or reStructuredText (`.rst`) files
68
+ - Markdown is recommended for simplicity
69
+ - Place new files in the appropriate subdirectory
70
+ - Update `index.rst` to add new sections to the navigation
71
+
72
+ ## Theme
73
+
74
+ The documentation uses the `sphinx_book_theme` with NVIDIA branding, matching the Isaac Lab documentation style.
GR00T-WholeBodyControl/gear_sonic.egg-info/PKG-INFO ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Metadata-Version: 2.4
2
+ Name: gear_sonic
3
+ Version: 0.1.0
4
+ Author: NVIDIA Gear Lab
5
+ License: Apache-2.0
6
+ Classifier: Intended Audience :: Science/Research
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: License :: OSI Approved :: Apache Software License
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
11
+ Requires-Python: >=3.10
12
+ Description-Content-Type: text/plain
13
+ Requires-Dist: numpy==1.26.4
14
+ Requires-Dist: scipy==1.15.3
15
+ Requires-Dist: torch>=2.4.0
16
+ Requires-Dist: joblib
17
+ Requires-Dist: tqdm
18
+ Requires-Dist: easydict
19
+ Requires-Dist: loguru
20
+ Provides-Extra: teleop
21
+ Requires-Dist: pyzmq; extra == "teleop"
22
+ Requires-Dist: msgpack; extra == "teleop"
23
+ Requires-Dist: msgpack-numpy; extra == "teleop"
24
+ Requires-Dist: pin; extra == "teleop"
25
+ Requires-Dist: pyvista; platform_machine != "aarch64" and extra == "teleop"
26
+ Provides-Extra: sim
27
+ Requires-Dist: mujoco; extra == "sim"
28
+ Requires-Dist: tyro; extra == "sim"
29
+ Requires-Dist: pin; extra == "sim"
30
+ Requires-Dist: pyyaml; extra == "sim"
31
+ Requires-Dist: pyzmq; extra == "sim"
32
+ Requires-Dist: msgpack; extra == "sim"
33
+ Requires-Dist: msgpack-numpy; extra == "sim"
34
+ Requires-Dist: opencv-python; extra == "sim"
35
+ Provides-Extra: data-collection
36
+ Requires-Dist: pyzmq; extra == "data-collection"
37
+ Requires-Dist: msgpack; extra == "data-collection"
38
+ Requires-Dist: msgpack-numpy; extra == "data-collection"
39
+ Requires-Dist: pin; extra == "data-collection"
40
+ Requires-Dist: tyro; extra == "data-collection"
41
+ Requires-Dist: pyttsx3==2.90; extra == "data-collection"
42
+ Requires-Dist: av>=14.2; extra == "data-collection"
43
+ Requires-Dist: opencv-python; extra == "data-collection"
44
+ Requires-Dist: lerobot @ git+https://github.com/huggingface/lerobot.git@a445d9c9da6bea99a8972daa4fe1fdd053d711d2 ; extra == "data-collection"
45
+ Requires-Dist: datasets==3.6.0; extra == "data-collection"
46
+ Provides-Extra: camera
47
+ Requires-Dist: pyzmq; extra == "camera"
48
+ Requires-Dist: msgpack; extra == "camera"
49
+ Requires-Dist: msgpack-numpy; extra == "camera"
50
+ Requires-Dist: opencv-python; extra == "camera"
51
+ Requires-Dist: tyro; extra == "camera"
52
+ Requires-Dist: depthai; extra == "camera"
53
+ Requires-Dist: requests; extra == "camera"
54
+ Provides-Extra: inference
55
+ Requires-Dist: pyzmq; extra == "inference"
56
+ Requires-Dist: msgpack; extra == "inference"
57
+ Requires-Dist: msgpack-numpy; extra == "inference"
58
+ Requires-Dist: pin; extra == "inference"
59
+ Requires-Dist: tyro; extra == "inference"
60
+ Requires-Dist: opencv-python; extra == "inference"
61
+ Requires-Dist: scipy; extra == "inference"
62
+ Requires-Dist: Isaac-GR00T @ git+https://github.com/NVIDIA/Isaac-GR00T.git ; extra == "inference"
63
+ Provides-Extra: training
64
+ Requires-Dist: hydra-core==1.3.2; extra == "training"
65
+ Requires-Dist: wandb; extra == "training"
66
+ Requires-Dist: trl==0.28.0; extra == "training"
67
+ Requires-Dist: transformers>=4.56.2; extra == "training"
68
+ Requires-Dist: accelerate>=1.3.0; extra == "training"
69
+ Requires-Dist: tensorboard; extra == "training"
70
+ Requires-Dist: smpl_sim @ git+https://github.com/ZhengyiLuo/SMPLSim.git ; extra == "training"
71
+
72
+ NVIDIA Gear Sonic - Whole Body Control
GR00T-WholeBodyControl/gear_sonic.egg-info/SOURCES.txt ADDED
@@ -0,0 +1,276 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ pyproject.toml
2
+ ../gear_sonic/__init__.py
3
+ ../gear_sonic/eval_agent_trl.py
4
+ ../gear_sonic/eval_exp.py
5
+ ../gear_sonic/train_agent_trl.py
6
+ ../gear_sonic/version.py
7
+ ../gear_sonic.egg-info/PKG-INFO
8
+ ../gear_sonic.egg-info/SOURCES.txt
9
+ ../gear_sonic.egg-info/dependency_links.txt
10
+ ../gear_sonic.egg-info/requires.txt
11
+ ../gear_sonic.egg-info/top_level.txt
12
+ ../gear_sonic/camera/__init__.py
13
+ ../gear_sonic/camera/composed_camera.py
14
+ ../gear_sonic/camera/sensor.py
15
+ ../gear_sonic/camera/sensor_server.py
16
+ ../gear_sonic/camera/drivers/__init__.py
17
+ ../gear_sonic/camera/drivers/dummy.py
18
+ ../gear_sonic/camera/drivers/oak.py
19
+ ../gear_sonic/camera/drivers/realsense.py
20
+ ../gear_sonic/camera/drivers/usb_camera.py
21
+ ../gear_sonic/config/base.yaml
22
+ ../gear_sonic/config/base_eval.yaml
23
+ ../gear_sonic/config/eval_exp.yaml
24
+ ../gear_sonic/config/actor_critic/mlp.yaml
25
+ ../gear_sonic/config/actor_critic/critics/mlp.yaml
26
+ ../gear_sonic/config/actor_critic/decoders/g1_dyn_mlp.yaml
27
+ ../gear_sonic/config/actor_critic/decoders/g1_kin_mf_mlp.yaml
28
+ ../gear_sonic/config/actor_critic/encoders/g1_mf_mlp.yaml
29
+ ../gear_sonic/config/actor_critic/encoders/smpl_mlp.yaml
30
+ ../gear_sonic/config/actor_critic/encoders/soma_mlp.yaml
31
+ ../gear_sonic/config/actor_critic/encoders/teleop_mlp.yaml
32
+ ../gear_sonic/config/actor_critic/quantizers/fsq.yaml
33
+ ../gear_sonic/config/actor_critic/universal_token/all_mlp_v1.yaml
34
+ ../gear_sonic/config/actor_critic/universal_token/all_mlp_v1_soma.yaml
35
+ ../gear_sonic/config/algo/ppo_im_phc.yaml
36
+ ../gear_sonic/config/algo/trl/ppo.yaml
37
+ ../gear_sonic/config/aux_losses/terms/g1_recon.yaml
38
+ ../gear_sonic/config/aux_losses/terms/g1_smpl_latent.yaml
39
+ ../gear_sonic/config/aux_losses/terms/g1_soma_latent.yaml
40
+ ../gear_sonic/config/aux_losses/terms/g1_teleop_latent.yaml
41
+ ../gear_sonic/config/aux_losses/terms/reencoded_smpl_g1_latent.yaml
42
+ ../gear_sonic/config/aux_losses/terms/teleop_smpl_latent.yaml
43
+ ../gear_sonic/config/aux_losses/universal_token/g1_recon_and_all_latent.yaml
44
+ ../gear_sonic/config/aux_losses/universal_token/g1_recon_and_all_latent_soma.yaml
45
+ ../gear_sonic/config/base/hydra.yaml
46
+ ../gear_sonic/config/base/structure.yaml
47
+ ../gear_sonic/config/callbacks/im_eval.yaml
48
+ ../gear_sonic/config/callbacks/im_resample.yaml
49
+ ../gear_sonic/config/callbacks/model_save.yaml
50
+ ../gear_sonic/config/callbacks/read_eval.yaml
51
+ ../gear_sonic/config/callbacks/wandb.yaml
52
+ ../gear_sonic/config/exp/manager/universal_token/all_modes/sonic_bones_seed.yaml
53
+ ../gear_sonic/config/exp/manager/universal_token/all_modes/sonic_h2.yaml
54
+ ../gear_sonic/config/exp/manager/universal_token/all_modes/sonic_release.yaml
55
+ ../gear_sonic/config/exp/manager/universal_token/all_modes/sonic_v1_1.yaml
56
+ ../gear_sonic/config/manager_env/base_env.yaml
57
+ ../gear_sonic/config/manager_env/actions/terms/joint_pos.yaml
58
+ ../gear_sonic/config/manager_env/actions/tracking/base.yaml
59
+ ../gear_sonic/config/manager_env/commands/terms/motion.yaml
60
+ ../gear_sonic/config/manager_env/commands/tracking/base.yaml
61
+ ../gear_sonic/config/manager_env/curriculum/empty.yaml
62
+ ../gear_sonic/config/manager_env/events/terms/add_joint_default_pos.yaml
63
+ ../gear_sonic/config/manager_env/events/terms/base_com.yaml
64
+ ../gear_sonic/config/manager_env/events/terms/physics_material.yaml
65
+ ../gear_sonic/config/manager_env/events/terms/push_robot.yaml
66
+ ../gear_sonic/config/manager_env/events/terms/randomize_rigid_body_mass.yaml
67
+ ../gear_sonic/config/manager_env/events/tracking/base.yaml
68
+ ../gear_sonic/config/manager_env/events/tracking/level0_4.yaml
69
+ ../gear_sonic/config/manager_env/observations/critic/privileged.yaml
70
+ ../gear_sonic/config/manager_env/observations/critic/privileged_mf_hist.yaml
71
+ ../gear_sonic/config/manager_env/observations/policy/global.yaml
72
+ ../gear_sonic/config/manager_env/observations/policy/local_dir_hist.yaml
73
+ ../gear_sonic/config/manager_env/observations/terms/actions.yaml
74
+ ../gear_sonic/config/manager_env/observations/terms/base_ang_vel.yaml
75
+ ../gear_sonic/config/manager_env/observations/terms/base_lin_vel.yaml
76
+ ../gear_sonic/config/manager_env/observations/terms/body_ori.yaml
77
+ ../gear_sonic/config/manager_env/observations/terms/body_pos.yaml
78
+ ../gear_sonic/config/manager_env/observations/terms/command.yaml
79
+ ../gear_sonic/config/manager_env/observations/terms/command_multi_future.yaml
80
+ ../gear_sonic/config/manager_env/observations/terms/command_multi_future_lower_body.yaml
81
+ ../gear_sonic/config/manager_env/observations/terms/command_multi_future_nonflat.yaml
82
+ ../gear_sonic/config/manager_env/observations/terms/command_z.yaml
83
+ ../gear_sonic/config/manager_env/observations/terms/command_z_multi_future_nonflat.yaml
84
+ ../gear_sonic/config/manager_env/observations/terms/encoder_index.yaml
85
+ ../gear_sonic/config/manager_env/observations/terms/gravity_dir.yaml
86
+ ../gear_sonic/config/manager_env/observations/terms/joint_pos.yaml
87
+ ../gear_sonic/config/manager_env/observations/terms/joint_pos_multi_future_wrist_for_smpl.yaml
88
+ ../gear_sonic/config/manager_env/observations/terms/joint_pos_multi_future_wrist_for_soma.yaml
89
+ ../gear_sonic/config/manager_env/observations/terms/joint_vel.yaml
90
+ ../gear_sonic/config/manager_env/observations/terms/motion_anchor_ori_b.yaml
91
+ ../gear_sonic/config/manager_env/observations/terms/motion_anchor_ori_b_mf_nonflat.yaml
92
+ ../gear_sonic/config/manager_env/observations/terms/motion_anchor_ori_heading.yaml
93
+ ../gear_sonic/config/manager_env/observations/terms/motion_anchor_ori_heading_mf_nonflat.yaml
94
+ ../gear_sonic/config/manager_env/observations/terms/motion_anchor_pos_b.yaml
95
+ ../gear_sonic/config/manager_env/observations/terms/smpl_joints_multi_future_local_nonflat.yaml
96
+ ../gear_sonic/config/manager_env/observations/terms/smpl_root_ori_b_multi_future.yaml
97
+ ../gear_sonic/config/manager_env/observations/terms/smpl_root_ori_heading_multi_future.yaml
98
+ ../gear_sonic/config/manager_env/observations/terms/soma_joints_multi_future_local_nonflat.yaml
99
+ ../gear_sonic/config/manager_env/observations/terms/soma_root_ori_b_multi_future.yaml
100
+ ../gear_sonic/config/manager_env/observations/terms/vr_3point_local_orn_target.yaml
101
+ ../gear_sonic/config/manager_env/observations/terms/vr_3point_local_target.yaml
102
+ ../gear_sonic/config/manager_env/observations/tokenizer/unitoken_all_noz.yaml
103
+ ../gear_sonic/config/manager_env/observations/tokenizer/unitoken_all_noz_heading.yaml
104
+ ../gear_sonic/config/manager_env/observations/tokenizer/unitoken_all_noz_soma.yaml
105
+ ../gear_sonic/config/manager_env/recorders/empty.yaml
106
+ ../gear_sonic/config/manager_env/recorders/render.yaml
107
+ ../gear_sonic/config/manager_env/rewards/terms/action_rate_l2.yaml
108
+ ../gear_sonic/config/manager_env/rewards/terms/anti_shake_ang_vel.yaml
109
+ ../gear_sonic/config/manager_env/rewards/terms/energy_consumption.yaml
110
+ ../gear_sonic/config/manager_env/rewards/terms/feet_acc.yaml
111
+ ../gear_sonic/config/manager_env/rewards/terms/joint_limit.yaml
112
+ ../gear_sonic/config/manager_env/rewards/terms/tracking_anchor_ori.yaml
113
+ ../gear_sonic/config/manager_env/rewards/terms/tracking_anchor_pos.yaml
114
+ ../gear_sonic/config/manager_env/rewards/terms/tracking_body_angvel.yaml
115
+ ../gear_sonic/config/manager_env/rewards/terms/tracking_body_linvel.yaml
116
+ ../gear_sonic/config/manager_env/rewards/terms/tracking_relative_body_ori.yaml
117
+ ../gear_sonic/config/manager_env/rewards/terms/tracking_relative_body_pos.yaml
118
+ ../gear_sonic/config/manager_env/rewards/terms/tracking_vr_2wrists_local_ori.yaml
119
+ ../gear_sonic/config/manager_env/rewards/terms/tracking_vr_5point_local.yaml
120
+ ../gear_sonic/config/manager_env/rewards/terms/undesired_contacts.yaml
121
+ ../gear_sonic/config/manager_env/rewards/tracking/base.yaml
122
+ ../gear_sonic/config/manager_env/rewards/tracking/base_5point_local_feet_acc.yaml
123
+ ../gear_sonic/config/manager_env/rewards/tracking/local_feet_acc_energy_5pt.yaml
124
+ ../gear_sonic/config/manager_env/terminations/terms/anchor_ori_full.yaml
125
+ ../gear_sonic/config/manager_env/terminations/terms/anchor_pos.yaml
126
+ ../gear_sonic/config/manager_env/terminations/terms/anchor_pos_adaptive.yaml
127
+ ../gear_sonic/config/manager_env/terminations/terms/ee_body_pos.yaml
128
+ ../gear_sonic/config/manager_env/terminations/terms/ee_body_pos_adaptive.yaml
129
+ ../gear_sonic/config/manager_env/terminations/terms/foot_pos_xyz.yaml
130
+ ../gear_sonic/config/manager_env/terminations/terms/motion_time_out.yaml
131
+ ../gear_sonic/config/manager_env/terminations/tracking/base.yaml
132
+ ../gear_sonic/config/manager_env/terminations/tracking/base_adaptive_strict_ori_foot_xyz.yaml
133
+ ../gear_sonic/config/manager_env/terminations/tracking/eval.yaml
134
+ ../gear_sonic/config/opt/wandb.yaml
135
+ ../gear_sonic/config/trainer/trl.yaml
136
+ ../gear_sonic/config/trainer/trl_ppo_aux.yaml
137
+ ../gear_sonic/data/exporter.py
138
+ ../gear_sonic/data/features_sonic_vla.py
139
+ ../gear_sonic/data/video_writer.py
140
+ ../gear_sonic/data/robot_model/__init__.py
141
+ ../gear_sonic/data/robot_model/robot_model.py
142
+ ../gear_sonic/data/robot_model/instantiation/__init__.py
143
+ ../gear_sonic/data/robot_model/instantiation/g1.py
144
+ ../gear_sonic/data/robot_model/supplemental_info/__init__.py
145
+ ../gear_sonic/data/robot_model/supplemental_info/robot_supplemental_info.py
146
+ ../gear_sonic/data/robot_model/supplemental_info/g1/__init__.py
147
+ ../gear_sonic/data/robot_model/supplemental_info/g1/g1_supplemental_info.py
148
+ ../gear_sonic/data_process/convert_soma_csv_to_motion_lib.py
149
+ ../gear_sonic/data_process/extract_soma_joints_from_bvh.py
150
+ ../gear_sonic/data_process/filter_and_copy_bones_data.py
151
+ ../gear_sonic/data_process/split_pkl_files.py
152
+ ../gear_sonic/envs/__init__.py
153
+ ../gear_sonic/envs/env_utils/__init__.py
154
+ ../gear_sonic/envs/env_utils/joint_utils.py
155
+ ../gear_sonic/envs/manager_env/__init__.py
156
+ ../gear_sonic/envs/manager_env/modular_tracking_env_cfg.py
157
+ ../gear_sonic/envs/manager_env/mdp/__init__.py
158
+ ../gear_sonic/envs/manager_env/mdp/actions.py
159
+ ../gear_sonic/envs/manager_env/mdp/actuators.py
160
+ ../gear_sonic/envs/manager_env/mdp/commands.py
161
+ ../gear_sonic/envs/manager_env/mdp/curriculum.py
162
+ ../gear_sonic/envs/manager_env/mdp/events.py
163
+ ../gear_sonic/envs/manager_env/mdp/observations.py
164
+ ../gear_sonic/envs/manager_env/mdp/recorders.py
165
+ ../gear_sonic/envs/manager_env/mdp/rewards.py
166
+ ../gear_sonic/envs/manager_env/mdp/terminations.py
167
+ ../gear_sonic/envs/manager_env/mdp/terrain.py
168
+ ../gear_sonic/envs/manager_env/mdp/utils.py
169
+ ../gear_sonic/envs/manager_env/robots/__init__.py
170
+ ../gear_sonic/envs/manager_env/robots/g1.py
171
+ ../gear_sonic/envs/manager_env/robots/h2.py
172
+ ../gear_sonic/envs/wrapper/__init__.py
173
+ ../gear_sonic/envs/wrapper/manager_env_wrapper.py
174
+ ../gear_sonic/examples/live_camera_teleop/soma_pt_to_sonic_v3.py
175
+ ../gear_sonic/examples/live_camera_teleop/soma_to_smpl.py
176
+ ../gear_sonic/examples/live_camera_teleop/webcam_stream.py
177
+ ../gear_sonic/isaac_utils/__init__.py
178
+ ../gear_sonic/isaac_utils/maths.py
179
+ ../gear_sonic/isaac_utils/rotations.py
180
+ ../gear_sonic/scripts/launch_data_collection.py
181
+ ../gear_sonic/scripts/launch_inference.py
182
+ ../gear_sonic/scripts/pico_manager_thread_server.py
183
+ ../gear_sonic/scripts/process_dataset.py
184
+ ../gear_sonic/scripts/run_camera_viewer.py
185
+ ../gear_sonic/scripts/run_data_exporter.py
186
+ ../gear_sonic/scripts/run_sim_loop.py
187
+ ../gear_sonic/scripts/run_vla_inference.py
188
+ ../gear_sonic/tests/test_input_readers.py
189
+ ../gear_sonic/trl/__init__.py
190
+ ../gear_sonic/trl/callbacks/__init__.py
191
+ ../gear_sonic/trl/callbacks/hv_callback_handler.py
192
+ ../gear_sonic/trl/callbacks/im_eval_callback.py
193
+ ../gear_sonic/trl/callbacks/im_resample_callback.py
194
+ ../gear_sonic/trl/callbacks/model_save_callback.py
195
+ ../gear_sonic/trl/callbacks/read_eval_callback.py
196
+ ../gear_sonic/trl/callbacks/wandb_callback.py
197
+ ../gear_sonic/trl/losses/__init__.py
198
+ ../gear_sonic/trl/losses/token_losses.py
199
+ ../gear_sonic/trl/modules/__init__.py
200
+ ../gear_sonic/trl/modules/actor_critic_modules.py
201
+ ../gear_sonic/trl/modules/base_module.py
202
+ ../gear_sonic/trl/modules/data_utils.py
203
+ ../gear_sonic/trl/modules/universal_token_modules.py
204
+ ../gear_sonic/trl/trainer/__init__.py
205
+ ../gear_sonic/trl/trainer/ppo_trainer.py
206
+ ../gear_sonic/trl/trainer/ppo_trainer_aux_loss.py
207
+ ../gear_sonic/trl/utils/__init__.py
208
+ ../gear_sonic/trl/utils/common.py
209
+ ../gear_sonic/trl/utils/data.py
210
+ ../gear_sonic/trl/utils/kornia_transform.py
211
+ ../gear_sonic/trl/utils/math.py
212
+ ../gear_sonic/trl/utils/order_converter.py
213
+ ../gear_sonic/trl/utils/rl.py
214
+ ../gear_sonic/trl/utils/rotation_conversion.py
215
+ ../gear_sonic/trl/utils/scheduler.py
216
+ ../gear_sonic/trl/utils/torch_transform.py
217
+ ../gear_sonic/trl/utils/smplx/smplx_utils.py
218
+ ../gear_sonic/trl/utils/smplx/body_model/__init__.py
219
+ ../gear_sonic/trl/utils/smplx/body_model/body_model.py
220
+ ../gear_sonic/trl/utils/smplx/body_model/body_model_smplh.py
221
+ ../gear_sonic/trl/utils/smplx/body_model/body_model_smplx.py
222
+ ../gear_sonic/trl/utils/smplx/body_model/min_lbs.py
223
+ ../gear_sonic/trl/utils/smplx/body_model/rotation_conversions.py
224
+ ../gear_sonic/trl/utils/smplx/body_model/smpl_lite.py
225
+ ../gear_sonic/trl/utils/smplx/body_model/smpl_vert_segmentation.json
226
+ ../gear_sonic/trl/utils/smplx/body_model/smplx_lite.py
227
+ ../gear_sonic/trl/utils/smplx/body_model/utils.py
228
+ ../gear_sonic/utils/__init__.py
229
+ ../gear_sonic/utils/average_meters.py
230
+ ../gear_sonic/utils/batch_normalizer.py
231
+ ../gear_sonic/utils/common.py
232
+ ../gear_sonic/utils/config_utils.py
233
+ ../gear_sonic/utils/inference_helpers.py
234
+ ../gear_sonic/utils/logging.py
235
+ ../gear_sonic/utils/obs_utils.py
236
+ ../gear_sonic/utils/running_mean_std.py
237
+ ../gear_sonic/utils/torch_utils.py
238
+ ../gear_sonic/utils/data_collection/__init__.py
239
+ ../gear_sonic/utils/data_collection/episode_state.py
240
+ ../gear_sonic/utils/data_collection/keyboard_subscriber.py
241
+ ../gear_sonic/utils/data_collection/telemetry.py
242
+ ../gear_sonic/utils/data_collection/text_to_speech.py
243
+ ../gear_sonic/utils/data_collection/transforms.py
244
+ ../gear_sonic/utils/data_collection/zmq_state_subscriber.py
245
+ ../gear_sonic/utils/inference/__init__.py
246
+ ../gear_sonic/utils/inference/initial_poses.py
247
+ ../gear_sonic/utils/inference/vla_utils.py
248
+ ../gear_sonic/utils/motion_lib/__init__.py
249
+ ../gear_sonic/utils/motion_lib/motion_lib_base.py
250
+ ../gear_sonic/utils/motion_lib/motion_lib_robot.py
251
+ ../gear_sonic/utils/motion_lib/skeleton.py
252
+ ../gear_sonic/utils/motion_lib/torch_humanoid_batch.py
253
+ ../gear_sonic/utils/mujoco_sim/__init__.py
254
+ ../gear_sonic/utils/mujoco_sim/base_sim.py
255
+ ../gear_sonic/utils/mujoco_sim/configs.py
256
+ ../gear_sonic/utils/mujoco_sim/image_publish_utils.py
257
+ ../gear_sonic/utils/mujoco_sim/metric_utils.py
258
+ ../gear_sonic/utils/mujoco_sim/robot.py
259
+ ../gear_sonic/utils/mujoco_sim/sensor_server.py
260
+ ../gear_sonic/utils/mujoco_sim/sim_utils.py
261
+ ../gear_sonic/utils/mujoco_sim/simulator_factory.py
262
+ ../gear_sonic/utils/mujoco_sim/unitree_sdk2py_bridge.py
263
+ ../gear_sonic/utils/mujoco_sim/wbc_configs/g1_29dof_sonic_model12.yaml
264
+ ../gear_sonic/utils/network/network_utils.py
265
+ ../gear_sonic/utils/teleop/input_readers.py
266
+ ../gear_sonic/utils/teleop/isaac_teleop_client.py
267
+ ../gear_sonic/utils/teleop/solver/solver.py
268
+ ../gear_sonic/utils/teleop/solver/hand/g1_gripper_ik_solver.py
269
+ ../gear_sonic/utils/teleop/vis/vr3pt_pose_visualizer.py
270
+ ../gear_sonic/utils/teleop/zmq/zmq_planner_sender.py
271
+ ../gear_sonic/utils/teleop/zmq/zmq_poller.py
272
+ ../gear_sonic_deploy/visualize_motion.py
273
+ ../gear_sonic_deploy/reference/convert_motions.py
274
+ ../gear_sonic_deploy/src/g1/g1_deploy_onnx_ref/tests/pose_estimation_server_onboard_test.py
275
+ ../gear_sonic_deploy/src/g1/g1_deploy_onnx_ref/tests/test_zmq_manager.py
276
+ tests/test_input_readers.py
GR00T-WholeBodyControl/gear_sonic.egg-info/dependency_links.txt ADDED
@@ -0,0 +1 @@
 
 
1
+
GR00T-WholeBodyControl/gear_sonic.egg-info/requires.txt ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ numpy==1.26.4
2
+ scipy==1.15.3
3
+ torch>=2.4.0
4
+ joblib
5
+ tqdm
6
+ easydict
7
+ loguru
8
+
9
+ [camera]
10
+ pyzmq
11
+ msgpack
12
+ msgpack-numpy
13
+ opencv-python
14
+ tyro
15
+ depthai
16
+ requests
17
+
18
+ [data_collection]
19
+ pyzmq
20
+ msgpack
21
+ msgpack-numpy
22
+ pin
23
+ tyro
24
+ pyttsx3==2.90
25
+ av>=14.2
26
+ opencv-python
27
+ lerobot @ git+https://github.com/huggingface/lerobot.git@a445d9c9da6bea99a8972daa4fe1fdd053d711d2
28
+ datasets==3.6.0
29
+
30
+ [inference]
31
+ pyzmq
32
+ msgpack
33
+ msgpack-numpy
34
+ pin
35
+ tyro
36
+ opencv-python
37
+ scipy
38
+ Isaac-GR00T @ git+https://github.com/NVIDIA/Isaac-GR00T.git
39
+
40
+ [sim]
41
+ mujoco
42
+ tyro
43
+ pin
44
+ pyyaml
45
+ pyzmq
46
+ msgpack
47
+ msgpack-numpy
48
+ opencv-python
49
+
50
+ [teleop]
51
+ pyzmq
52
+ msgpack
53
+ msgpack-numpy
54
+ pin
55
+
56
+ [teleop:platform_machine != "aarch64"]
57
+ pyvista
58
+
59
+ [training]
60
+ hydra-core==1.3.2
61
+ wandb
62
+ trl==0.28.0
63
+ transformers>=4.56.2
64
+ accelerate>=1.3.0
65
+ tensorboard
66
+ smpl_sim @ git+https://github.com/ZhengyiLuo/SMPLSim.git
GR00T-WholeBodyControl/gear_sonic.egg-info/top_level.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ gear_sonic
2
+ gear_sonic_deploy
GR00T-WholeBodyControl/gear_sonic/__init__.py ADDED
File without changes
GR00T-WholeBodyControl/gear_sonic/camera/__init__.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Camera server package for streaming camera images over ZMQ.
2
+
3
+ Runs on the robot computer and publishes JPEG-encoded frames that the
4
+ data exporter (on the workstation) subscribes to for recording.
5
+
6
+ Quickstart (on robot)::
7
+
8
+ bash install_scripts/install_camera_server.sh
9
+ source .venv_camera/bin/activate
10
+ python -m gear_sonic.camera.composed_camera --ego-view-camera oak
11
+
12
+ See ``docs/source/tutorials/data_collection.md`` for full setup instructions.
13
+ """
14
+
15
+ from gear_sonic.camera.sensor_server import (
16
+ CameraMountPosition,
17
+ ImageMessageSchema,
18
+ ImageUtils,
19
+ SensorClient,
20
+ SensorServer,
21
+ )
22
+
23
+ __all__ = [
24
+ "CameraMountPosition",
25
+ "ImageMessageSchema",
26
+ "ImageUtils",
27
+ "SensorClient",
28
+ "SensorServer",
29
+ ]
GR00T-WholeBodyControl/gear_sonic/camera/composed_camera.py ADDED
@@ -0,0 +1,692 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Composed camera server — orchestrates multiple camera drivers.
2
+
3
+ Runs each camera in its own thread with staggered initialization and
4
+ automatic reconnection. Publishes all frames as a single merged
5
+ ``ImageMessageSchema`` payload over ZMQ.
6
+
7
+ Usage (on robot)::
8
+
9
+ python -m gear_sonic.camera.composed_camera \\
10
+ --ego-view-camera oak \\
11
+ --ego-view-device-id 18443010E1ABC12300 \\
12
+ --port 5555
13
+
14
+ Supported camera types: ``oak``, ``oak_mono``, ``realsense``,
15
+ ``usb``, or a path to an ``.mp4`` file for replay testing.
16
+
17
+ Run ``python -m gear_sonic.camera.composed_camera --help`` for all options.
18
+ """
19
+
20
+ from collections import deque
21
+ from dataclasses import dataclass
22
+ import queue
23
+ import threading
24
+ import time
25
+ from typing import Any
26
+
27
+ import cv2 # noqa: F401 — imported early to avoid TSL segfault with camera SDKs
28
+ import numpy as np
29
+
30
+ from gear_sonic.camera.sensor import Sensor
31
+ from gear_sonic.camera.sensor_server import (
32
+ CameraMountPosition,
33
+ ImageMessageSchema,
34
+ SensorClient,
35
+ SensorServer,
36
+ )
37
+
38
+
39
+ def read_qr_code(data):
40
+ """Measure end-to-end latency by decoding QR-code timestamps."""
41
+ current_time = time.monotonic()
42
+ detector = cv2.QRCodeDetector()
43
+ for key, img in data["images"].items():
44
+ decoded_time, bbox, _ = detector.detectAndDecode(img)
45
+ if bbox is not None and decoded_time:
46
+ print(f"{key} latency: {(current_time - float(decoded_time)) * 1e3:.1f} ms")
47
+ else:
48
+ print(f"{key} QR code not detected.")
49
+
50
+
51
+ @dataclass
52
+ class ComposedCameraConfig:
53
+ """Camera configuration for the composed camera server."""
54
+
55
+ ego_view_camera: str | None = "oak"
56
+ """Camera type for ego view: oak, oak_mono, realsense, zed, usb, or None."""
57
+
58
+ ego_view_device_id: str | None = None
59
+ """Device ID for ego view camera (OAK MxID, RealSense serial, USB /dev/video index)."""
60
+
61
+ head_camera: str | None = None
62
+ """Camera type for head view."""
63
+
64
+ head_device_id: str | None = None
65
+ """Device ID for head camera."""
66
+
67
+ left_wrist_camera: str | None = None
68
+ """Camera type for left wrist view."""
69
+
70
+ left_wrist_device_id: str | None = None
71
+ """Device ID for left wrist camera."""
72
+
73
+ right_wrist_camera: str | None = None
74
+ """Camera type for right wrist view."""
75
+
76
+ right_wrist_device_id: str | None = None
77
+ """Device ID for right wrist camera."""
78
+
79
+ fps: int = 30
80
+ """Publish rate. OAK cameras run at 30 FPS; lower values add latency."""
81
+
82
+ run_as_server: bool = True
83
+ """Run as ZMQ PUB server (set False for in-process usage)."""
84
+
85
+ server: bool = True
86
+ """Alias for run_as_server kept for backward compatibility."""
87
+
88
+ port: int = 5555
89
+ """ZMQ port for server / client communication."""
90
+
91
+ test_latency: bool = False
92
+ """Decode QR-code timestamps in each frame to measure latency."""
93
+
94
+ queue_size: int = 3
95
+ """Per-camera image queue depth."""
96
+
97
+ use_mjpeg: bool = False
98
+ """Use on-device MJPEG encoding on OAK cameras to reduce USB bandwidth."""
99
+
100
+ mjpeg_quality: int = 80
101
+ """MJPEG quality 1-100 (only when use_mjpeg=True)."""
102
+
103
+ def __post_init__(self):
104
+ self.run_as_server = self.server
105
+
106
+
107
+ class ComposedCameraSensor(Sensor, SensorServer):
108
+ """Multi-camera orchestrator with per-camera threads and auto-reconnect."""
109
+
110
+ def __init__(self, config: ComposedCameraConfig):
111
+ self.config = config
112
+ self.camera_queues: dict[str, queue.Queue] = {}
113
+ self.camera_threads: dict[str, threading.Thread] = {}
114
+ self.shutdown_events: dict[str, threading.Event] = {}
115
+ self.error_events: dict[str, threading.Event] = {}
116
+ self.error_messages: dict[str, str] = {}
117
+ self._observation_spaces: dict[str, Any] = {}
118
+
119
+ camera_configs = self._get_camera_configs()
120
+
121
+ for _idx, (mount_position, camera_config) in enumerate(camera_configs.items()):
122
+ camera_queue = queue.Queue(maxsize=config.queue_size)
123
+ shutdown_event = threading.Event()
124
+ error_event = threading.Event()
125
+
126
+ self.camera_queues[mount_position] = camera_queue
127
+ self.shutdown_events[mount_position] = shutdown_event
128
+ self.error_events[mount_position] = error_event
129
+
130
+ thread = threading.Thread(
131
+ target=self._camera_worker_wrapper,
132
+ args=(
133
+ mount_position,
134
+ camera_config["camera_type"],
135
+ camera_config["device_id"],
136
+ camera_queue,
137
+ shutdown_event,
138
+ error_event,
139
+ ),
140
+ )
141
+ thread.start()
142
+ self.camera_threads[mount_position] = thread
143
+
144
+ # Stagger init to avoid USB bandwidth contention
145
+ init_timeout = 15.0
146
+ init_start = time.time()
147
+ while time.time() - init_start < init_timeout:
148
+ if mount_position in self._observation_spaces:
149
+ print(f"[{mount_position}] Camera ready, waiting 3s before next camera...")
150
+ time.sleep(3.0)
151
+ break
152
+ time.sleep(0.5)
153
+ else:
154
+ print(f"[{mount_position}] Camera init timeout, proceeding anyway...")
155
+
156
+ if config.run_as_server:
157
+ print("Waiting for all cameras to be ready before starting server...")
158
+ self._wait_for_all_cameras_ready(timeout=60.0)
159
+ self.start_server(config.port)
160
+
161
+ def _get_camera_configs(self) -> dict[str, dict]:
162
+ camera_configs = {}
163
+
164
+ if self.config.ego_view_camera is not None:
165
+ camera_configs[CameraMountPosition.EGO_VIEW.value] = {
166
+ "camera_type": self.config.ego_view_camera,
167
+ "device_id": self.config.ego_view_device_id,
168
+ }
169
+
170
+ if self.config.head_camera is not None:
171
+ camera_configs[CameraMountPosition.HEAD.value] = {
172
+ "camera_type": self.config.head_camera,
173
+ "device_id": self.config.head_device_id,
174
+ }
175
+
176
+ if self.config.left_wrist_camera is not None:
177
+ camera_configs[CameraMountPosition.LEFT_WRIST.value] = {
178
+ "camera_type": self.config.left_wrist_camera,
179
+ "device_id": self.config.left_wrist_device_id,
180
+ }
181
+
182
+ if self.config.right_wrist_camera is not None:
183
+ camera_configs[CameraMountPosition.RIGHT_WRIST.value] = {
184
+ "camera_type": self.config.right_wrist_camera,
185
+ "device_id": self.config.right_wrist_device_id,
186
+ }
187
+
188
+ return camera_configs
189
+
190
+ def _wait_for_all_cameras_ready(self, timeout: float = 60.0):
191
+ expected_cameras = set(self.camera_queues.keys())
192
+ start_time = time.time()
193
+
194
+ while time.time() - start_time < timeout:
195
+ ready_cameras = set()
196
+ for mount_position, camera_queue in self.camera_queues.items():
197
+ if not camera_queue.empty():
198
+ ready_cameras.add(mount_position)
199
+
200
+ if ready_cameras == expected_cameras:
201
+ print(f"All {len(expected_cameras)} cameras ready: {ready_cameras}")
202
+ time.sleep(1.0)
203
+ return
204
+
205
+ waiting_for = expected_cameras - ready_cameras
206
+ print(
207
+ f"Waiting for cameras: {waiting_for} "
208
+ f"({len(ready_cameras)}/{len(expected_cameras)} ready)"
209
+ )
210
+ time.sleep(2.0)
211
+
212
+ ready_cameras = set()
213
+ for mount_position, camera_queue in self.camera_queues.items():
214
+ if not camera_queue.empty() or mount_position in self._observation_spaces:
215
+ ready_cameras.add(mount_position)
216
+ missing = expected_cameras - ready_cameras
217
+ print(
218
+ f"[WARNING] Timeout waiting for all cameras. "
219
+ f"Missing: {missing}. Starting anyway with: {ready_cameras}"
220
+ )
221
+
222
+ def _camera_worker_wrapper(
223
+ self,
224
+ mount_position: str,
225
+ camera_type: str,
226
+ device_id: str | None,
227
+ image_queue: queue.Queue,
228
+ shutdown_event: threading.Event,
229
+ error_event: threading.Event,
230
+ ):
231
+ """Worker thread with auto-reconnection."""
232
+ max_init_retries = 10
233
+ max_reconnect_attempts = 5
234
+ reconnect_count = 0
235
+
236
+ while not shutdown_event.is_set() and reconnect_count < max_reconnect_attempts:
237
+ camera = None
238
+ try:
239
+ init_retry_delay = 1.0
240
+
241
+ for attempt in range(max_init_retries):
242
+ if shutdown_event.is_set():
243
+ return
244
+
245
+ try:
246
+ if reconnect_count > 0:
247
+ print(
248
+ f"[{mount_position}] Reconnecting camera "
249
+ f"(reconnect {reconnect_count}/{max_reconnect_attempts}, "
250
+ f"attempt {attempt + 1}/{max_init_retries})..."
251
+ )
252
+ else:
253
+ print(
254
+ f"[{mount_position}] Initializing camera "
255
+ f"(attempt {attempt + 1}/{max_init_retries})..."
256
+ )
257
+ camera = self._instantiate_camera(mount_position, camera_type, device_id)
258
+ print(f"[{mount_position}] Camera initialized successfully")
259
+ break
260
+ except Exception as e:
261
+ print(f"[{mount_position}] Camera init failed: {e}")
262
+ if attempt < max_init_retries - 1:
263
+ print(f"[{mount_position}] Retrying in {init_retry_delay:.1f}s...")
264
+ time.sleep(init_retry_delay)
265
+ init_retry_delay = min(init_retry_delay * 1.5, 10.0)
266
+ else:
267
+ raise RuntimeError(
268
+ f"Camera {mount_position} ({camera_type}) failed to initialize "
269
+ f"after {max_init_retries} attempts: {e}"
270
+ )
271
+
272
+ obs_space = camera.observation_space()
273
+ if obs_space is not None:
274
+ self._observation_spaces[mount_position] = obs_space
275
+ else:
276
+ self._observation_spaces[mount_position] = True
277
+
278
+ consecutive_failures = 0
279
+ max_consecutive_failures = 10
280
+ warmup_period = True
281
+ warmup_start_time = time.time()
282
+ warmup_timeout = 5.0
283
+
284
+ while not shutdown_event.is_set():
285
+ try:
286
+ frame = camera.read()
287
+ except Exception as e:
288
+ print(f"[{mount_position}] Frame read exception: {e}")
289
+ frame = None
290
+ consecutive_failures = max_consecutive_failures
291
+
292
+ if frame:
293
+ consecutive_failures = 0
294
+ warmup_period = False
295
+ try:
296
+ image_queue.put_nowait(frame)
297
+ except queue.Full:
298
+ try:
299
+ image_queue.get_nowait()
300
+ image_queue.put_nowait(frame)
301
+ except queue.Empty:
302
+ pass
303
+ else:
304
+ if warmup_period:
305
+ if time.time() - warmup_start_time > warmup_timeout:
306
+ print(
307
+ f"[{mount_position}] Warmup timeout — will attempt reconnect"
308
+ )
309
+ break
310
+ time.sleep(0.1)
311
+ else:
312
+ consecutive_failures += 1
313
+ if consecutive_failures >= max_consecutive_failures:
314
+ print(
315
+ f"[{mount_position}] Too many consecutive failures "
316
+ f"({consecutive_failures}) — will attempt reconnect"
317
+ )
318
+ break
319
+ time.sleep(0.01)
320
+
321
+ if camera is not None:
322
+ try:
323
+ camera.close()
324
+ except Exception as e:
325
+ print(f"[{mount_position}] Error closing camera: {e}")
326
+ camera = None
327
+
328
+ if not shutdown_event.is_set():
329
+ reconnect_count += 1
330
+ print(f"[{mount_position}] Waiting 5 seconds before reconnect attempt...")
331
+ time.sleep(5.0)
332
+
333
+ except Exception as e:
334
+ print(f"[{mount_position}] Camera error: {e}")
335
+ if camera is not None:
336
+ try:
337
+ camera.close()
338
+ except Exception:
339
+ pass
340
+ camera = None
341
+
342
+ if not shutdown_event.is_set():
343
+ reconnect_count += 1
344
+ if reconnect_count < max_reconnect_attempts:
345
+ print(
346
+ f"[{mount_position}] Waiting 5 seconds before reconnect "
347
+ f"attempt {reconnect_count}/{max_reconnect_attempts}..."
348
+ )
349
+ time.sleep(5.0)
350
+
351
+ if reconnect_count >= max_reconnect_attempts and not shutdown_event.is_set():
352
+ error_msg = (
353
+ f"Camera {mount_position} ({camera_type}) failed "
354
+ f"after {max_reconnect_attempts} reconnect attempts"
355
+ )
356
+ print(f"[ERROR] {error_msg}")
357
+ self.error_messages[mount_position] = error_msg
358
+ error_event.set()
359
+
360
+ def _instantiate_camera(
361
+ self, mount_position: str, camera_type: str, device_id: str | None = None
362
+ ) -> Sensor:
363
+ """Instantiate a camera sensor based on camera_type (lazy imports)."""
364
+ if camera_type in ("oak", "oak_mono"):
365
+ from gear_sonic.camera.drivers.oak import OAKConfig, OAKSensor
366
+
367
+ oak_config = OAKConfig()
368
+ oak_config.use_mjpeg = self.config.use_mjpeg
369
+ oak_config.mjpeg_quality = self.config.mjpeg_quality
370
+ if camera_type == "oak_mono":
371
+ oak_config.enable_mono_cameras = True
372
+ print(f"Initializing OAK sensor for camera type: {camera_type}")
373
+ return OAKSensor(config=oak_config, mount_position=mount_position, device_id=device_id)
374
+
375
+ elif camera_type == "realsense":
376
+ from gear_sonic.camera.drivers.realsense import RealSenseSensor
377
+
378
+ print(f"Initializing RealSense sensor for camera type: {camera_type}")
379
+ return RealSenseSensor(mount_position=mount_position)
380
+
381
+ elif camera_type.endswith(".mp4"):
382
+ from gear_sonic.camera.drivers.dummy import ReplayDummySensor
383
+
384
+ print(f"Initializing Replay Dummy Sensor for camera type: {camera_type}")
385
+ return ReplayDummySensor(video_path=camera_type)
386
+
387
+ elif camera_type == "usb":
388
+ from gear_sonic.camera.drivers.usb_camera import USBCameraConfig, USBCameraSensor
389
+
390
+ usb_config = USBCameraConfig()
391
+ device_idx = int(device_id) if device_id else 0
392
+ print(f"Initializing USB camera for type: {camera_type}, device: {device_idx}")
393
+ return USBCameraSensor(
394
+ config=usb_config, mount_position=mount_position, device_index=device_idx
395
+ )
396
+
397
+ else:
398
+ raise ValueError(f"Unsupported camera type: {camera_type}")
399
+
400
+ def _check_for_errors(self):
401
+ for mount_position, error_event in self.error_events.items():
402
+ if error_event.is_set():
403
+ error_msg = self.error_messages.get(
404
+ mount_position, f"Camera {mount_position} encountered an unknown error"
405
+ )
406
+ raise RuntimeError(error_msg)
407
+
408
+ def read(self):
409
+ """Read frames from all cameras. Returns None unless ALL cameras have frames."""
410
+ self._check_for_errors()
411
+
412
+ expected_cameras = set(self.camera_queues.keys())
413
+ message = {}
414
+
415
+ for mount_position, camera_queue in self.camera_queues.items():
416
+ frame = self._get_latest_from_queue(camera_queue)
417
+ if frame is not None:
418
+ message[mount_position] = frame
419
+
420
+ if set(message.keys()) == expected_cameras:
421
+ return message
422
+ return None
423
+
424
+ def _get_latest_from_queue(self, camera_queue: queue.Queue) -> dict[str, Any] | None:
425
+ latest = None
426
+ try:
427
+ while True:
428
+ latest = camera_queue.get_nowait()
429
+ except queue.Empty:
430
+ pass
431
+ return latest
432
+
433
+ def close(self):
434
+ for shutdown_event in self.shutdown_events.values():
435
+ shutdown_event.set()
436
+ for thread in self.camera_threads.values():
437
+ thread.join(timeout=5.0)
438
+ for camera_queue in self.camera_queues.values():
439
+ try:
440
+ while True:
441
+ camera_queue.get_nowait()
442
+ except queue.Empty:
443
+ pass
444
+ if self.config.run_as_server:
445
+ self.stop_server()
446
+
447
+ def serialize(self, data: dict[str, Any]) -> dict[str, Any]:
448
+ raise NotImplementedError("Use serialize_message() for ComposedCameraSensor")
449
+
450
+ def serialize_message(self, message: dict[str, Any]) -> dict[str, Any]:
451
+ """Merge per-camera data into a single ImageMessageSchema."""
452
+ all_timestamps = {}
453
+ all_images = {}
454
+ for _mount, camera_data in message.items():
455
+ all_timestamps.update(camera_data.get("timestamps", {}))
456
+ all_images.update(camera_data.get("images", {}))
457
+ img_schema = ImageMessageSchema(timestamps=all_timestamps, images=all_images)
458
+ return img_schema.serialize()
459
+
460
+ def run_server(self):
461
+ """Main server loop — reads, serializes and publishes frames."""
462
+ idx = 0
463
+ server_start_time = time.monotonic()
464
+ fps_print_time = time.monotonic()
465
+ frame_interval = 1.0 / self.config.fps
466
+
467
+ while True:
468
+ target_time = server_start_time + (idx + 1) * frame_interval
469
+
470
+ message = self.read()
471
+ if message:
472
+ if self.config.test_latency:
473
+ read_qr_code(message)
474
+
475
+ serialized_message = self.serialize_message(message)
476
+ self.send_message(serialized_message)
477
+ idx += 1
478
+
479
+ if idx % 10 == 0:
480
+ print(f"Image sending FPS: {10 / (time.monotonic() - fps_print_time):.2f}")
481
+ fps_print_time = time.monotonic()
482
+
483
+ current_time = time.monotonic()
484
+ sleep_time = target_time - current_time
485
+ if sleep_time > 0:
486
+ time.sleep(sleep_time)
487
+ else:
488
+ if not message:
489
+ idx += 1
490
+
491
+ def observation_space(self):
492
+ try:
493
+ import gymnasium as gym
494
+
495
+ return gym.spaces.Dict(self._observation_spaces)
496
+ except ImportError:
497
+ return None
498
+
499
+
500
+ class ComposedCameraClientSensor(Sensor, SensorClient):
501
+ """ZMQ client that deserializes merged camera frames from the server."""
502
+
503
+ def __init__(self, server_ip: str = "localhost", port: int = 5555):
504
+ self.start_client(server_ip, port)
505
+
506
+ self._latest_message = None
507
+ self._avg_time_per_frame: deque = deque(maxlen=20)
508
+ self._msg_received_time = 0
509
+ self._start_time = 0.0
510
+ self.idx = 0
511
+
512
+ self._last_new_message_time = None
513
+ self._last_staleness_warning_time = 0.0
514
+ self._staleness_warning_interval = 2.0
515
+
516
+ print("Initialized composed camera client sensor")
517
+
518
+ def read(self, blocking: bool = False, **kwargs) -> dict[str, Any] | None:
519
+ self._start_time = time.time()
520
+ current_time = time.time()
521
+
522
+ if blocking:
523
+ message = self.receive_message()
524
+ if not message:
525
+ return None
526
+ else:
527
+ message = self.receive_message_nonblocking(timeout_ms=0)
528
+
529
+ if message is not None:
530
+ self.idx += 1
531
+ self._latest_message = ImageMessageSchema.deserialize(message).asdict()
532
+ self._last_new_message_time = current_time
533
+
534
+ if self.idx % 10 == 0:
535
+ for image_key, image_time in self._latest_message["timestamps"].items():
536
+ image_latency = (time.time() - image_time) * 1000
537
+ print(f"Image latency for {image_key}: {image_latency:.2f} ms")
538
+
539
+ self._msg_received_time = time.time()
540
+ self._avg_time_per_frame.append(self._msg_received_time - self._start_time)
541
+ elif not blocking and self._latest_message is not None:
542
+ if self._last_new_message_time is not None:
543
+ time_since_last_message = current_time - self._last_new_message_time
544
+ if time_since_last_message > 0.1:
545
+ if (
546
+ current_time - self._last_staleness_warning_time
547
+ >= self._staleness_warning_interval
548
+ ):
549
+ print(
550
+ f"[WARNING] No new image message received for "
551
+ f"{time_since_last_message*1000:.1f}ms. "
552
+ f"Reusing stale image. Check camera server connection."
553
+ )
554
+ self._last_staleness_warning_time = current_time
555
+
556
+ return self._latest_message
557
+
558
+ def serialize(self, data: dict[str, Any]) -> dict[str, Any]:
559
+ raise NotImplementedError("Client does not serialize")
560
+
561
+ def close(self):
562
+ self.stop_client()
563
+
564
+ def fps(self) -> float:
565
+ if len(self._avg_time_per_frame) == 0:
566
+ return 0.0
567
+ return float(1 / np.mean(self._avg_time_per_frame))
568
+
569
+
570
+ class _MjpegGrabber:
571
+ """Background thread that reads an MJPEG stream via raw HTTP."""
572
+
573
+ def __init__(self, url: str):
574
+ self.url = url
575
+ self.lock = threading.Lock()
576
+ self.frame: np.ndarray | None = None
577
+ self._running = True
578
+ self._thread = threading.Thread(target=self._run, daemon=True)
579
+ self._thread.start()
580
+
581
+ def _run(self):
582
+ import requests
583
+
584
+ resp = requests.get(self.url, stream=True, timeout=10)
585
+ buf = b""
586
+ for chunk in resp.iter_content(chunk_size=4096):
587
+ if not self._running:
588
+ break
589
+ buf += chunk
590
+ while True:
591
+ soi = buf.find(b"\xff\xd8")
592
+ if soi == -1:
593
+ break
594
+ eoi = buf.find(b"\xff\xd9", soi + 2)
595
+ if eoi == -1:
596
+ break
597
+ jpeg_bytes = buf[soi : eoi + 2]
598
+ buf = buf[eoi + 2 :]
599
+ frame = cv2.imdecode(
600
+ np.frombuffer(jpeg_bytes, dtype=np.uint8), cv2.IMREAD_COLOR
601
+ )
602
+ if frame is not None:
603
+ with self.lock:
604
+ self.frame = frame
605
+
606
+ def get(self) -> np.ndarray | None:
607
+ with self.lock:
608
+ return self.frame
609
+
610
+ def stop(self):
611
+ self._running = False
612
+ self._thread.join(timeout=2)
613
+
614
+
615
+ class ComposedCameraHttpClient:
616
+ """Camera client that reads MJPEG streams over HTTP.
617
+
618
+ Drop-in replacement for :class:`ComposedCameraClientSensor` when cameras
619
+ are served via an HTTP MJPEG server.
620
+
621
+ Usage::
622
+
623
+ client = ComposedCameraHttpClient("http://<ROBOT_IP>:8000")
624
+ data = client.read() # {"images": {"left": ndarray, ...}, "timestamps": {...}}
625
+ """
626
+
627
+ DEFAULT_NAME_MAP = {
628
+ "center": "ego_view",
629
+ "left": "left_wrist",
630
+ "right": "right_wrist",
631
+ }
632
+
633
+ def __init__(self, base_url: str, camera_name_map: dict[str, str] | None = None):
634
+ self.base_url = base_url.rstrip("/")
635
+ self.camera_name_map = (
636
+ camera_name_map if camera_name_map is not None else self.DEFAULT_NAME_MAP
637
+ )
638
+ self.camera_names: list[str] = []
639
+ self._grabbers: dict[str, _MjpegGrabber] = {}
640
+ self._connect()
641
+
642
+ def _connect(self):
643
+ import requests
644
+
645
+ resp = requests.get(f"{self.base_url}/cameras", timeout=5)
646
+ resp.raise_for_status()
647
+ self.camera_names = resp.json()
648
+ print(f"HTTP MJPEG: discovered cameras: {self.camera_names}")
649
+ for name in self.camera_names:
650
+ url = f"{self.base_url}/stream/{name}"
651
+ self._grabbers[name] = _MjpegGrabber(url)
652
+
653
+ def read(self, blocking: bool = False, **kwargs) -> dict[str, Any] | None:
654
+ images = {}
655
+ any_ok = False
656
+ for name, grabber in self._grabbers.items():
657
+ frame = grabber.get()
658
+ mapped_name = self.camera_name_map.get(name, name)
659
+ if frame is not None:
660
+ images[mapped_name] = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
661
+ any_ok = True
662
+ else:
663
+ images[mapped_name] = None
664
+ if not any_ok:
665
+ return None
666
+ return {"images": images, "timestamps": {n: time.time() for n in images}}
667
+
668
+ def close(self):
669
+ for grabber in self._grabbers.values():
670
+ grabber.stop()
671
+
672
+
673
+ if __name__ == "__main__":
674
+ import tyro
675
+
676
+ config = tyro.cli(ComposedCameraConfig)
677
+
678
+ if config.run_as_server:
679
+ composed_camera = ComposedCameraSensor(config)
680
+ print("Running composed camera server...")
681
+ composed_camera.run_server()
682
+ else:
683
+ composed_client = ComposedCameraClientSensor(server_ip="localhost", port=config.port)
684
+ try:
685
+ while True:
686
+ data = composed_client.read()
687
+ if data is not None:
688
+ print(f"FPS: {composed_client.fps():.2f}")
689
+ time.sleep(0.1)
690
+ except KeyboardInterrupt:
691
+ print("Stopping client...")
692
+ composed_client.close()
GR00T-WholeBodyControl/gear_sonic/config/base.yaml ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # First we define the global structures that will be used by all the configs.
2
+ defaults:
3
+ # - base/fabric
4
+ - _self_
5
+ - base/hydra
6
+ - base/structure
7
+ - callbacks/model_save
8
+ - callbacks/wandb
9
+ - trainer: trl
10
+ - opt/wandb
11
+
12
+ num_gpus: 1
13
+ max_retries: 1
14
+
15
+ # These are global variables that all levels of the config can access.
16
+ ## Experiment setup
17
+ seed: 0
18
+ codebase_version: 1.0 # this is recorded to enable auto-conversion of models between different versions of the codebase
19
+ headless: True
20
+ num_envs: 4096
21
+
22
+ ### Checkpoint logic
23
+ auto_load_latest: False
24
+ checkpoint: null
25
+
26
+ ### Naming and dir structure
27
+ project_name: TEST
28
+ experiment_name: TEST
29
+
30
+ base_dir: logs_rl
31
+ timestamp: ${now:%Y%m%d_%H%M%S}
32
+ experiment_dir: ${base_dir}/${project_name}/$${experiment_name}-${timestamp}
33
+ save_dir: ${experiment_dir}/.hydra
34
+
35
+ force_flat_terrain: False
36
+
37
+ use_wandb: false
38
+ log_task_name: TEST
39
+
40
+ multi_gpu: False
41
+ global_rank: 0
42
+
43
+ ### Simulation
44
+ sim_type: isaacsim
45
+ env_spacing: 20
46
+ output_dir: ${experiment_dir}/output
47
+
48
+ eval_overrides:
49
+ headless: False
50
+ num_envs: 1
51
+ auto_load_latest: False
52
+ use_wandb: False
GR00T-WholeBodyControl/gear_sonic/config/base_eval.yaml ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # @package _global_
2
+
3
+ defaults:
4
+ - /callbacks/im_eval
5
+ - manager_env/recorders: empty
6
+ - _self_
7
+
8
+ checkpoint: ???
9
+
10
+ algo:
11
+ config:
12
+ eval:
13
+ num_eval_episodes: 150
14
+ save_videos: false
15
+ video_save_prob: 1.0
16
+ save_goal_reached_only: true
17
+ save_trajectories: false
18
+ num_save_episodes: 200
19
+
20
+ eval_timestamp: ${now:%Y%m%d_%H%M%S}
21
+ eval_name: TEST
22
+ eval_base_dir: logs_eval
23
+ eval_log_dir: ${eval_base_dir}/${eval_timestamp}-${eval_name}
24
+ hydra:
25
+ run:
26
+ dir: ${eval_log_dir}
GR00T-WholeBodyControl/gear_sonic/config/eval_exp.yaml ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # @package _global_
2
+
3
+ # Required parameter: experiment directory to monitor
4
+ experiment_dir: ???
5
+ max_train_steps: 1000000
6
+
7
+ # Number of environments for metrics eval (multi-GPU accelerate)
8
+ num_eval_envs: 3072
9
+
10
+ # Number of environments for render eval (single GPU)
11
+ # VRAM scales ~linearly: 64 envs ≈ 23GB, 32 envs ≈ 12GB
12
+ # Keep ≤ 64 on L40 (49GB VRAM) to avoid OOM
13
+ num_render_videos: 64
14
+ num_test_render_videos: 32
15
+
16
+ # Monitoring configuration
17
+ scan_interval: 0 # seconds between scans for new checkpoints
18
+ checkpoint_ready_delay: 60 # seconds to wait after checkpoint modification before evaluating
19
+
20
+ eval_frequency: null # null = eval every checkpoint
21
+ eval_last_n: null # null = eval all; integer = only eval last N checkpoints
22
+ eval_callbacks: im_eval
23
+ eval_datasets: null
24
+ eval_modes: [null]
25
+
26
+ # Single pass mode: evaluate pending checkpoints once and exit
27
+ single_pass: false
28
+
29
+ # Extra overrides to pass to eval_agent_trl.py (list of strings)
30
+ # Example: ["++manager_env.commands.motion.start_from_first_frame=true"]
31
+ extra_overrides: []
32
+
33
+ # Set to true to suppress eval subprocess output
34
+ capture_output: true
GR00T-WholeBodyControl/gear_sonic/data/robot_model/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """Robot model package: Pinocchio-based FK/IK models for the G1 humanoid."""
2
+
3
+ from .robot_model import ReducedRobotModel, RobotModel
4
+
5
+ __all__ = ["RobotModel", "ReducedRobotModel"]
GR00T-WholeBodyControl/gear_sonic/data/robot_model/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (360 Bytes). View file
 
GR00T-WholeBodyControl/gear_sonic/data/robot_model/__pycache__/robot_model.cpython-310.pyc ADDED
Binary file (24.3 kB). View file
 
GR00T-WholeBodyControl/gear_sonic/data/robot_model/instantiation/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ """G1 robot model instantiation helpers."""
2
+
3
+ from .g1 import instantiate_g1_robot_model
4
+
5
+ __all__ = ["instantiate_g1_robot_model"]
GR00T-WholeBodyControl/gear_sonic/data/robot_model/instantiation/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (313 Bytes). View file
 
GR00T-WholeBodyControl/gear_sonic/data/robot_model/instantiation/__pycache__/g1.cpython-310.pyc ADDED
Binary file (2.05 kB). View file
 
GR00T-WholeBodyControl/gear_sonic/data/robot_model/instantiation/g1.py ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Factory function to instantiate a configured G1 RobotModel from URDF."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+ from typing import Literal
6
+
7
+ from gear_sonic.data.robot_model.robot_model import RobotModel
8
+ from gear_sonic.data.robot_model.supplemental_info.g1.g1_supplemental_info import (
9
+ ElbowPose,
10
+ G1SupplementalInfo,
11
+ WaistLocation,
12
+ )
13
+
14
+
15
+ def instantiate_g1_robot_model(
16
+ waist_location: Literal["lower_body", "upper_body", "lower_and_upper_body"] = "lower_body",
17
+ high_elbow_pose: bool = False,
18
+ ):
19
+ """
20
+ Instantiate a G1 robot model with configurable waist location and pose.
21
+
22
+ Args:
23
+ waist_location: Whether to put waist in "lower_body" (default G1 behavior),
24
+ "upper_body" (waist controlled with arms/manipulation via IK),
25
+ or "lower_and_upper_body" (waist reference from arms/manipulation
26
+ via IK then passed to lower body policy)
27
+ high_elbow_pose: Whether to use high elbow pose configuration for default joint positions
28
+
29
+ Returns:
30
+ RobotModel: Configured G1 robot model
31
+ """
32
+ model_data_dir = Path(__file__).resolve().parent.parent / "model_data" / "g1"
33
+ robot_model_config = {
34
+ "asset_path": str(model_data_dir),
35
+ "urdf_path": str(model_data_dir / "g1_29dof_with_hand.urdf"),
36
+ }
37
+ assert waist_location in [
38
+ "lower_body",
39
+ "upper_body",
40
+ "lower_and_upper_body",
41
+ ], f"Invalid waist_location: {waist_location}. Must be 'lower_body' or 'upper_body' or 'lower_and_upper_body'"
42
+
43
+ # Map string values to enums
44
+ waist_location_enum = {
45
+ "lower_body": WaistLocation.LOWER_BODY,
46
+ "upper_body": WaistLocation.UPPER_BODY,
47
+ "lower_and_upper_body": WaistLocation.LOWER_AND_UPPER_BODY,
48
+ }[waist_location]
49
+
50
+ elbow_pose_enum = ElbowPose.HIGH if high_elbow_pose else ElbowPose.LOW
51
+
52
+ # Create single configurable supplemental info instance
53
+ robot_model_supplemental_info = G1SupplementalInfo(
54
+ waist_location=waist_location_enum, elbow_pose=elbow_pose_enum
55
+ )
56
+
57
+ robot_model = RobotModel(
58
+ robot_model_config["urdf_path"],
59
+ robot_model_config["asset_path"],
60
+ supplemental_info=robot_model_supplemental_info,
61
+ )
62
+ return robot_model
GR00T-WholeBodyControl/gear_sonic/data/robot_model/model_data/g1/g1_29dof_with_hand.urdf ADDED
@@ -0,0 +1,1497 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <robot name="g1_29dof_with_hand">
2
+ <mujoco>
3
+ <compiler meshdir="meshes" discardvisual="false"/>
4
+ </mujoco>
5
+
6
+ <!-- [CAUTION] uncomment when convert to mujoco -->
7
+ <!-- <link name="world"></link>
8
+ <joint name="floating_base_joint" type="floating">
9
+ <parent link="world"/>
10
+ <child link="pelvis"/>
11
+ </joint> -->
12
+
13
+ <link name="pelvis">
14
+ <inertial>
15
+ <origin xyz="0 0 -0.07605" rpy="0 0 0"/>
16
+ <mass value="3.813"/>
17
+ <inertia ixx="0.010549" ixy="0" ixz="2.1E-06" iyy="0.0093089" iyz="0" izz="0.0079184"/>
18
+ </inertial>
19
+ <visual>
20
+ <origin xyz="0 0 0" rpy="0 0 0"/>
21
+ <geometry>
22
+ <mesh filename="meshes/pelvis.STL"/>
23
+ </geometry>
24
+ <material name="dark">
25
+ <color rgba="0.2 0.2 0.2 1"/>
26
+ </material>
27
+ </visual>
28
+ </link>
29
+ <link name="pelvis_contour_link">
30
+ <inertial>
31
+ <origin xyz="0 0 0" rpy="0 0 0"/>
32
+ <mass value="0.001"/>
33
+ <inertia ixx="1e-7" ixy="0" ixz="0" iyy="1e-7" iyz="0" izz="1e-7"/>
34
+ </inertial>
35
+ <visual>
36
+ <origin xyz="0 0 0" rpy="0 0 0"/>
37
+ <geometry>
38
+ <mesh filename="meshes/pelvis_contour_link.STL"/>
39
+ </geometry>
40
+ <material name="white">
41
+ <color rgba="0.7 0.7 0.7 1"/>
42
+ </material>
43
+ </visual>
44
+ <collision>
45
+ <origin xyz="0 0 0" rpy="0 0 0"/>
46
+ <geometry>
47
+ <mesh filename="meshes/pelvis_contour_link.STL"/>
48
+ </geometry>
49
+ </collision>
50
+ </link>
51
+ <joint name="pelvis_contour_joint" type="fixed">
52
+ <parent link="pelvis"/>
53
+ <child link="pelvis_contour_link"/>
54
+ </joint>
55
+
56
+ <!-- Legs -->
57
+ <link name="left_hip_pitch_link">
58
+ <inertial>
59
+ <origin xyz="0.002741 0.047791 -0.02606" rpy="0 0 0"/>
60
+ <mass value="1.35"/>
61
+ <inertia ixx="0.001811" ixy="3.68E-05" ixz="-3.44E-05" iyy="0.0014193" iyz="0.000171" izz="0.0012812"/>
62
+ </inertial>
63
+ <visual>
64
+ <origin xyz="0 0 0" rpy="0 0 0"/>
65
+ <geometry>
66
+ <mesh filename="meshes/left_hip_pitch_link.STL"/>
67
+ </geometry>
68
+ <material name="dark">
69
+ <color rgba="0.2 0.2 0.2 1"/>
70
+ </material>
71
+ </visual>
72
+ <collision>
73
+ <origin xyz="0 0 0" rpy="0 0 0"/>
74
+ <geometry>
75
+ <mesh filename="meshes/left_hip_pitch_link.STL"/>
76
+ </geometry>
77
+ </collision>
78
+ </link>
79
+ <joint name="left_hip_pitch_joint" type="revolute">
80
+ <origin xyz="0 0.064452 -0.1027" rpy="0 0 0"/>
81
+ <parent link="pelvis"/>
82
+ <child link="left_hip_pitch_link"/>
83
+ <axis xyz="0 1 0"/>
84
+ <limit lower="-2.5307" upper="2.8798" effort="88" velocity="32"/>
85
+ </joint>
86
+ <link name="left_hip_roll_link">
87
+ <inertial>
88
+ <origin xyz="0.029812 -0.001045 -0.087934" rpy="0 0 0"/>
89
+ <mass value="1.52"/>
90
+ <inertia ixx="0.0023773" ixy="-3.8E-06" ixz="-0.0003908" iyy="0.0024123" iyz="1.84E-05" izz="0.0016595"/>
91
+ </inertial>
92
+ <visual>
93
+ <origin xyz="0 0 0" rpy="0 0 0"/>
94
+ <geometry>
95
+ <mesh filename="meshes/left_hip_roll_link.STL"/>
96
+ </geometry>
97
+ <material name="white">
98
+ <color rgba="0.7 0.7 0.7 1"/>
99
+ </material>
100
+ </visual>
101
+ <collision>
102
+ <origin xyz="0 0 0" rpy="0 0 0"/>
103
+ <geometry>
104
+ <mesh filename="meshes/left_hip_roll_link.STL"/>
105
+ </geometry>
106
+ </collision>
107
+ </link>
108
+ <joint name="left_hip_roll_joint" type="revolute">
109
+ <origin xyz="0 0.052 -0.030465" rpy="0 -0.1749 0"/>
110
+ <parent link="left_hip_pitch_link"/>
111
+ <child link="left_hip_roll_link"/>
112
+ <axis xyz="1 0 0"/>
113
+ <limit lower="-0.5236" upper="2.9671" effort="88" velocity="32"/>
114
+ </joint>
115
+ <link name="left_hip_yaw_link">
116
+ <inertial>
117
+ <origin xyz="-0.057709 -0.010981 -0.15078" rpy="0 0 0"/>
118
+ <mass value="1.702"/>
119
+ <inertia ixx="0.0057774" ixy="-0.0005411" ixz="-0.0023948" iyy="0.0076124" iyz="-0.0007072" izz="0.003149"/>
120
+ </inertial>
121
+ <visual>
122
+ <origin xyz="0 0 0" rpy="0 0 0"/>
123
+ <geometry>
124
+ <mesh filename="meshes/left_hip_yaw_link.STL"/>
125
+ </geometry>
126
+ <material name="white">
127
+ <color rgba="0.7 0.7 0.7 1"/>
128
+ </material>
129
+ </visual>
130
+ <collision>
131
+ <origin xyz="0 0 0" rpy="0 0 0"/>
132
+ <geometry>
133
+ <mesh filename="meshes/left_hip_yaw_link.STL"/>
134
+ </geometry>
135
+ </collision>
136
+ </link>
137
+ <joint name="left_hip_yaw_joint" type="revolute">
138
+ <origin xyz="0.025001 0 -0.12412" rpy="0 0 0"/>
139
+ <parent link="left_hip_roll_link"/>
140
+ <child link="left_hip_yaw_link"/>
141
+ <axis xyz="0 0 1"/>
142
+ <limit lower="-2.7576" upper="2.7576" effort="88" velocity="32"/>
143
+ </joint>
144
+ <link name="left_knee_link">
145
+ <inertial>
146
+ <origin xyz="0.005457 0.003964 -0.12074" rpy="0 0 0"/>
147
+ <mass value="1.932"/>
148
+ <inertia ixx="0.011329" ixy="4.82E-05" ixz="-4.49E-05" iyy="0.011277" iyz="-0.0007146" izz="0.0015168"/>
149
+ </inertial>
150
+ <visual>
151
+ <origin xyz="0 0 0" rpy="0 0 0"/>
152
+ <geometry>
153
+ <mesh filename="meshes/left_knee_link.STL"/>
154
+ </geometry>
155
+ <material name="white">
156
+ <color rgba="0.7 0.7 0.7 1"/>
157
+ </material>
158
+ </visual>
159
+ <collision>
160
+ <origin xyz="0 0 0" rpy="0 0 0"/>
161
+ <geometry>
162
+ <mesh filename="meshes/left_knee_link.STL"/>
163
+ </geometry>
164
+ </collision>
165
+ </link>
166
+ <joint name="left_knee_joint" type="revolute">
167
+ <origin xyz="-0.078273 0.0021489 -0.17734" rpy="0 0.1749 0"/>
168
+ <parent link="left_hip_yaw_link"/>
169
+ <child link="left_knee_link"/>
170
+ <axis xyz="0 1 0"/>
171
+ <limit lower="-0.087267" upper="2.8798" effort="139" velocity="20"/>
172
+ </joint>
173
+ <link name="left_ankle_pitch_link">
174
+ <inertial>
175
+ <origin xyz="-0.007269 0 0.011137" rpy="0 0 0"/>
176
+ <mass value="0.074"/>
177
+ <inertia ixx="8.4E-06" ixy="0" ixz="-2.9E-06" iyy="1.89E-05" iyz="0" izz="1.26E-05"/>
178
+ </inertial>
179
+ <visual>
180
+ <origin xyz="0 0 0" rpy="0 0 0"/>
181
+ <geometry>
182
+ <mesh filename="meshes/left_ankle_pitch_link.STL"/>
183
+ </geometry>
184
+ <material name="white">
185
+ <color rgba="0.7 0.7 0.7 1"/>
186
+ </material>
187
+ </visual>
188
+ <collision>
189
+ <origin xyz="0 0 0" rpy="0 0 0"/>
190
+ <geometry>
191
+ <mesh filename="meshes/left_ankle_pitch_link.STL"/>
192
+ </geometry>
193
+ </collision>
194
+ </link>
195
+ <joint name="left_ankle_pitch_joint" type="revolute">
196
+ <origin xyz="0 -9.4445E-05 -0.30001" rpy="0 0 0"/>
197
+ <parent link="left_knee_link"/>
198
+ <child link="left_ankle_pitch_link"/>
199
+ <axis xyz="0 1 0"/>
200
+ <limit lower="-0.87267" upper="0.5236" effort="50" velocity="37"/>
201
+ </joint>
202
+ <link name="left_ankle_roll_link">
203
+ <inertial>
204
+ <origin xyz="0.026505 0 -0.016425" rpy="0 0 0"/>
205
+ <mass value="0.608"/>
206
+ <inertia ixx="0.0002231" ixy="2E-07" ixz="8.91E-05" iyy="0.0016161" iyz="-1E-07" izz="0.0016667"/>
207
+ </inertial>
208
+ <visual>
209
+ <origin xyz="0 0 0" rpy="0 0 0"/>
210
+ <geometry>
211
+ <mesh filename="meshes/left_ankle_roll_link.STL"/>
212
+ </geometry>
213
+ <material name="dark">
214
+ <color rgba="0.2 0.2 0.2 1"/>
215
+ </material>
216
+ </visual>
217
+ <collision>
218
+ <origin xyz="-0.05 0.025 -0.03" rpy="0 0 0"/>
219
+ <geometry>
220
+ <sphere radius="0.005"/>
221
+ </geometry>
222
+ </collision>
223
+ <collision>
224
+ <origin xyz="-0.05 -0.025 -0.03" rpy="0 0 0"/>
225
+ <geometry>
226
+ <sphere radius="0.005"/>
227
+ </geometry>
228
+ </collision>
229
+ <collision>
230
+ <origin xyz="0.12 0.03 -0.03" rpy="0 0 0"/>
231
+ <geometry>
232
+ <sphere radius="0.005"/>
233
+ </geometry>
234
+ </collision>
235
+ <collision>
236
+ <origin xyz="0.12 -0.03 -0.03" rpy="0 0 0"/>
237
+ <geometry>
238
+ <sphere radius="0.005"/>
239
+ </geometry>
240
+ </collision>
241
+ </link>
242
+ <joint name="left_ankle_roll_joint" type="revolute">
243
+ <origin xyz="0 0 -0.017558" rpy="0 0 0"/>
244
+ <parent link="left_ankle_pitch_link"/>
245
+ <child link="left_ankle_roll_link"/>
246
+ <axis xyz="1 0 0"/>
247
+ <limit lower="-0.2618" upper="0.2618" effort="50" velocity="37"/>
248
+ </joint>
249
+ <link name="right_hip_pitch_link">
250
+ <inertial>
251
+ <origin xyz="0.002741 -0.047791 -0.02606" rpy="0 0 0"/>
252
+ <mass value="1.35"/>
253
+ <inertia ixx="0.001811" ixy="-3.68E-05" ixz="-3.44E-05" iyy="0.0014193" iyz="-0.000171" izz="0.0012812"/>
254
+ </inertial>
255
+ <visual>
256
+ <origin xyz="0 0 0" rpy="0 0 0"/>
257
+ <geometry>
258
+ <mesh filename="meshes/right_hip_pitch_link.STL"/>
259
+ </geometry>
260
+ <material name="dark">
261
+ <color rgba="0.2 0.2 0.2 1"/>
262
+ </material>
263
+ </visual>
264
+ <collision>
265
+ <origin xyz="0 0 0" rpy="0 0 0"/>
266
+ <geometry>
267
+ <mesh filename="meshes/right_hip_pitch_link.STL"/>
268
+ </geometry>
269
+ </collision>
270
+ </link>
271
+ <joint name="right_hip_pitch_joint" type="revolute">
272
+ <origin xyz="0 -0.064452 -0.1027" rpy="0 0 0"/>
273
+ <parent link="pelvis"/>
274
+ <child link="right_hip_pitch_link"/>
275
+ <axis xyz="0 1 0"/>
276
+ <limit lower="-2.5307" upper="2.8798" effort="88" velocity="32"/>
277
+ </joint>
278
+ <link name="right_hip_roll_link">
279
+ <inertial>
280
+ <origin xyz="0.029812 0.001045 -0.087934" rpy="0 0 0"/>
281
+ <mass value="1.52"/>
282
+ <inertia ixx="0.0023773" ixy="3.8E-06" ixz="-0.0003908" iyy="0.0024123" iyz="-1.84E-05" izz="0.0016595"/>
283
+ </inertial>
284
+ <visual>
285
+ <origin xyz="0 0 0" rpy="0 0 0"/>
286
+ <geometry>
287
+ <mesh filename="meshes/right_hip_roll_link.STL"/>
288
+ </geometry>
289
+ <material name="white">
290
+ <color rgba="0.7 0.7 0.7 1"/>
291
+ </material>
292
+ </visual>
293
+ <collision>
294
+ <origin xyz="0 0 0" rpy="0 0 0"/>
295
+ <geometry>
296
+ <mesh filename="meshes/right_hip_roll_link.STL"/>
297
+ </geometry>
298
+ </collision>
299
+ </link>
300
+ <joint name="right_hip_roll_joint" type="revolute">
301
+ <origin xyz="0 -0.052 -0.030465" rpy="0 -0.1749 0"/>
302
+ <parent link="right_hip_pitch_link"/>
303
+ <child link="right_hip_roll_link"/>
304
+ <axis xyz="1 0 0"/>
305
+ <limit lower="-2.9671" upper="0.5236" effort="88" velocity="32"/>
306
+ </joint>
307
+ <link name="right_hip_yaw_link">
308
+ <inertial>
309
+ <origin xyz="-0.057709 0.010981 -0.15078" rpy="0 0 0"/>
310
+ <mass value="1.702"/>
311
+ <inertia ixx="0.0057774" ixy="0.0005411" ixz="-0.0023948" iyy="0.0076124" iyz="0.0007072" izz="0.003149"/>
312
+ </inertial>
313
+ <visual>
314
+ <origin xyz="0 0 0" rpy="0 0 0"/>
315
+ <geometry>
316
+ <mesh filename="meshes/right_hip_yaw_link.STL"/>
317
+ </geometry>
318
+ <material name="white">
319
+ <color rgba="0.7 0.7 0.7 1"/>
320
+ </material>
321
+ </visual>
322
+ <collision>
323
+ <origin xyz="0 0 0" rpy="0 0 0"/>
324
+ <geometry>
325
+ <mesh filename="meshes/right_hip_yaw_link.STL"/>
326
+ </geometry>
327
+ </collision>
328
+ </link>
329
+ <joint name="right_hip_yaw_joint" type="revolute">
330
+ <origin xyz="0.025001 0 -0.12412" rpy="0 0 0"/>
331
+ <parent link="right_hip_roll_link"/>
332
+ <child link="right_hip_yaw_link"/>
333
+ <axis xyz="0 0 1"/>
334
+ <limit lower="-2.7576" upper="2.7576" effort="88" velocity="32"/>
335
+ </joint>
336
+ <link name="right_knee_link">
337
+ <inertial>
338
+ <origin xyz="0.005457 -0.003964 -0.12074" rpy="0 0 0"/>
339
+ <mass value="1.932"/>
340
+ <inertia ixx="0.011329" ixy="-4.82E-05" ixz="4.49E-05" iyy="0.011277" iyz="0.0007146" izz="0.0015168"/>
341
+ </inertial>
342
+ <visual>
343
+ <origin xyz="0 0 0" rpy="0 0 0"/>
344
+ <geometry>
345
+ <mesh filename="meshes/right_knee_link.STL"/>
346
+ </geometry>
347
+ <material name="white">
348
+ <color rgba="0.7 0.7 0.7 1"/>
349
+ </material>
350
+ </visual>
351
+ <collision>
352
+ <origin xyz="0 0 0" rpy="0 0 0"/>
353
+ <geometry>
354
+ <mesh filename="meshes/right_knee_link.STL"/>
355
+ </geometry>
356
+ </collision>
357
+ </link>
358
+ <joint name="right_knee_joint" type="revolute">
359
+ <origin xyz="-0.078273 -0.0021489 -0.17734" rpy="0 0.1749 0"/>
360
+ <parent link="right_hip_yaw_link"/>
361
+ <child link="right_knee_link"/>
362
+ <axis xyz="0 1 0"/>
363
+ <limit lower="-0.087267" upper="2.8798" effort="139" velocity="20"/>
364
+ </joint>
365
+ <link name="right_ankle_pitch_link">
366
+ <inertial>
367
+ <origin xyz="-0.007269 0 0.011137" rpy="0 0 0"/>
368
+ <mass value="0.074"/>
369
+ <inertia ixx="8.4E-06" ixy="0" ixz="-2.9E-06" iyy="1.89E-05" iyz="0" izz="1.26E-05"/>
370
+ </inertial>
371
+ <visual>
372
+ <origin xyz="0 0 0" rpy="0 0 0"/>
373
+ <geometry>
374
+ <mesh filename="meshes/right_ankle_pitch_link.STL"/>
375
+ </geometry>
376
+ <material name="white">
377
+ <color rgba="0.7 0.7 0.7 1"/>
378
+ </material>
379
+ </visual>
380
+ <collision>
381
+ <origin xyz="0 0 0" rpy="0 0 0"/>
382
+ <geometry>
383
+ <mesh filename="meshes/right_ankle_pitch_link.STL"/>
384
+ </geometry>
385
+ </collision>
386
+ </link>
387
+ <joint name="right_ankle_pitch_joint" type="revolute">
388
+ <origin xyz="0 9.4445E-05 -0.30001" rpy="0 0 0"/>
389
+ <parent link="right_knee_link"/>
390
+ <child link="right_ankle_pitch_link"/>
391
+ <axis xyz="0 1 0"/>
392
+ <limit lower="-0.87267" upper="0.5236" effort="50" velocity="37"/>
393
+ </joint>
394
+ <link name="right_ankle_roll_link">
395
+ <inertial>
396
+ <origin xyz="0.026505 0 -0.016425" rpy="0 0 0"/>
397
+ <mass value="0.608"/>
398
+ <inertia ixx="0.0002231" ixy="-2E-07" ixz="8.91E-05" iyy="0.0016161" iyz="1E-07" izz="0.0016667"/>
399
+ </inertial>
400
+ <visual>
401
+ <origin xyz="0 0 0" rpy="0 0 0"/>
402
+ <geometry>
403
+ <mesh filename="meshes/right_ankle_roll_link.STL"/>
404
+ </geometry>
405
+ <material name="dark">
406
+ <color rgba="0.2 0.2 0.2 1"/>
407
+ </material>
408
+ </visual>
409
+ <collision>
410
+ <origin xyz="-0.05 0.025 -0.03" rpy="0 0 0"/>
411
+ <geometry>
412
+ <sphere radius="0.005"/>
413
+ </geometry>
414
+ </collision>
415
+ <collision>
416
+ <origin xyz="-0.05 -0.025 -0.03" rpy="0 0 0"/>
417
+ <geometry>
418
+ <sphere radius="0.005"/>
419
+ </geometry>
420
+ </collision>
421
+ <collision>
422
+ <origin xyz="0.12 0.03 -0.03" rpy="0 0 0"/>
423
+ <geometry>
424
+ <sphere radius="0.005"/>
425
+ </geometry>
426
+ </collision>
427
+ <collision>
428
+ <origin xyz="0.12 -0.03 -0.03" rpy="0 0 0"/>
429
+ <geometry>
430
+ <sphere radius="0.005"/>
431
+ </geometry>
432
+ </collision>
433
+ </link>
434
+ <joint name="right_ankle_roll_joint" type="revolute">
435
+ <origin xyz="0 0 -0.017558" rpy="0 0 0"/>
436
+ <parent link="right_ankle_pitch_link"/>
437
+ <child link="right_ankle_roll_link"/>
438
+ <axis xyz="1 0 0"/>
439
+ <limit lower="-0.2618" upper="0.2618" effort="50" velocity="37"/>
440
+ </joint>
441
+
442
+ <!-- Torso -->
443
+ <link name="waist_yaw_link">
444
+ <inertial>
445
+ <origin xyz="0.003964 0 0.018769" rpy="0 0 0"/>
446
+ <mass value="0.244"/>
447
+ <inertia ixx="9.9587E-05" ixy="-1.833E-06" ixz="-1.2617E-05" iyy="0.00012411" iyz="-1.18E-07" izz="0.00015586"/>
448
+ </inertial>
449
+ <visual>
450
+ <origin xyz="0 0 0" rpy="0 0 0"/>
451
+ <geometry>
452
+ <mesh filename="meshes/waist_yaw_link.STL"/>
453
+ </geometry>
454
+ <material name="white">
455
+ <color rgba="0.7 0.7 0.7 1"/>
456
+ </material>
457
+ </visual>
458
+ </link>
459
+ <joint name="waist_yaw_joint" type="revolute">
460
+ <origin xyz="0 0 0" rpy="0 0 0"/>
461
+ <parent link="pelvis"/>
462
+ <child link="waist_yaw_link"/>
463
+ <axis xyz="0 0 1"/>
464
+ <limit lower="-2.618" upper="2.618" effort="88" velocity="32"/>
465
+ </joint>
466
+ <link name="waist_roll_link">
467
+ <inertial>
468
+ <origin xyz="0 -0.000236 0.010111" rpy="0 0 0"/>
469
+ <mass value="0.047"/>
470
+ <inertia ixx="7.515E-06" ixy="0" ixz="0" iyy="6.398E-06" iyz="9.9E-08" izz="3.988E-06"/>
471
+ </inertial>
472
+ <visual>
473
+ <origin xyz="0 0 0" rpy="0 0 0"/>
474
+ <geometry>
475
+ <mesh filename="meshes/waist_roll_link.STL"/>
476
+ </geometry>
477
+ <material name="white">
478
+ <color rgba="0.7 0.7 0.7 1"/>
479
+ </material>
480
+ </visual>
481
+ </link>
482
+ <joint name="waist_roll_joint" type="revolute">
483
+ <origin xyz="-0.0039635 0 0.035" rpy="0 0 0"/>
484
+ <parent link="waist_yaw_link"/>
485
+ <child link="waist_roll_link"/>
486
+ <axis xyz="1 0 0"/>
487
+ <limit lower="-0.52" upper="0.52" effort="50" velocity="37"/>
488
+ </joint>
489
+ <link name="torso_link">
490
+ <inertial>
491
+ <origin xyz="0.002601 0.000257 0.153719" rpy="0 0 0"/>
492
+ <mass value="8.562"/>
493
+ <inertia ixx="0.065674966" ixy="-8.597E-05" ixz="-0.001737252" iyy="0.053535188" iyz="8.6899E-05" izz="0.030808125"/>
494
+ </inertial>
495
+ <visual>
496
+ <origin xyz="0 0 0" rpy="0 0 0"/>
497
+ <geometry>
498
+ <mesh filename="meshes/torso_link.STL"/>
499
+ </geometry>
500
+ <material name="white">
501
+ <color rgba="0.7 0.7 0.7 1"/>
502
+ </material>
503
+ </visual>
504
+ <collision>
505
+ <origin xyz="0 0 0" rpy="0 0 0"/>
506
+ <geometry>
507
+ <mesh filename="meshes/torso_link.STL"/>
508
+ </geometry>
509
+ </collision>
510
+ </link>
511
+ <joint name="waist_pitch_joint" type="revolute">
512
+ <origin xyz="0 0 0.019" rpy="0 0 0"/>
513
+ <parent link="waist_roll_link"/>
514
+ <child link="torso_link"/>
515
+ <axis xyz="0 1 0"/>
516
+ <limit lower="-0.52" upper="0.52" effort="50" velocity="37"/>
517
+ </joint>
518
+
519
+ <!-- LOGO -->
520
+ <joint name="logo_joint" type="fixed">
521
+ <origin xyz="0.0039635 0 -0.054" rpy="0 0 0"/>
522
+ <parent link="torso_link"/>
523
+ <child link="logo_link"/>
524
+ </joint>
525
+ <link name="logo_link">
526
+ <inertial>
527
+ <origin xyz="0 0 0" rpy="0 0 0"/>
528
+ <mass value="0.001"/>
529
+ <inertia ixx="1e-7" ixy="0" ixz="0" iyy="1e-7" iyz="0" izz="1e-7"/>
530
+ </inertial>
531
+ <visual>
532
+ <origin xyz="0 0 0" rpy="0 0 0"/>
533
+ <geometry>
534
+ <mesh filename="meshes/logo_link.STL"/>
535
+ </geometry>
536
+ <material name="dark">
537
+ <color rgba="0.2 0.2 0.2 1"/>
538
+ </material>
539
+ </visual>
540
+ <collision>
541
+ <origin xyz="0 0 0" rpy="0 0 0"/>
542
+ <geometry>
543
+ <mesh filename="meshes/logo_link.STL"/>
544
+ </geometry>
545
+ </collision>
546
+ </link>
547
+
548
+ <!-- Head -->
549
+ <link name="head_link">
550
+ <inertial>
551
+ <origin xyz="0.005267 0.000299 0.449869" rpy="0 0 0"/>
552
+ <mass value="1.036"/>
553
+ <inertia ixx="0.004085051" ixy="-2.543E-06" ixz="-6.9455E-05" iyy="0.004185212" iyz="-3.726E-06" izz="0.001807911"/>
554
+ </inertial>
555
+ <visual>
556
+ <origin xyz="0 0 0" rpy="0 0 0"/>
557
+ <geometry>
558
+ <mesh filename="meshes/head_link.STL"/>
559
+ </geometry>
560
+ <material name="dark">
561
+ <color rgba="0.2 0.2 0.2 1"/>
562
+ </material>
563
+ </visual>
564
+ <collision>
565
+ <origin xyz="0 0 0" rpy="0 0 0"/>
566
+ <geometry>
567
+ <mesh filename="meshes/head_link.STL"/>
568
+ </geometry>
569
+ </collision>
570
+ </link>
571
+ <joint name="head_joint" type="fixed">
572
+ <origin xyz="0.0039635 0 -0.054" rpy="0 0 0"/>
573
+ <parent link="torso_link"/>
574
+ <child link="head_link"/>
575
+ </joint>
576
+
577
+ <!-- Waist Support -->
578
+ <link name="waist_support_link">
579
+ <inertial>
580
+ <origin xyz="0 0 0" rpy="0 0 0"/>
581
+ <mass value="0.001"/>
582
+ <inertia ixx="1e-7" ixy="0" ixz="0" iyy="1e-7" iyz="0" izz="1e-7"/>
583
+ </inertial>
584
+ <visual>
585
+ <origin xyz="0 0 0" rpy="0 0 0"/>
586
+ <geometry>
587
+ <mesh filename="meshes/waist_support_link.STL"/>
588
+ </geometry>
589
+ <material name="white">
590
+ <color rgba="0.7 0.7 0.7 1"/>
591
+ </material>
592
+ </visual>
593
+ <collision>
594
+ <origin xyz="0 0 0" rpy="0 0 0"/>
595
+ <geometry>
596
+ <mesh filename="meshes/waist_support_link.STL"/>
597
+ </geometry>
598
+ </collision>
599
+ </link>
600
+ <joint name="waist_support_joint" type="fixed">
601
+ <origin xyz="0.0039635 0 -0.054" rpy="0 0 0"/>
602
+ <parent link="torso_link"/>
603
+ <child link="waist_support_link"/>
604
+ </joint>
605
+
606
+ <!-- IMU -->
607
+ <link name="imu_link"></link>
608
+ <joint name="imu_joint" type="fixed">
609
+ <origin xyz="-0.03959 -0.00224 0.13792" rpy="0 0 0"/>
610
+ <parent link="torso_link"/>
611
+ <child link="imu_link"/>
612
+ </joint>
613
+
614
+ <!-- d435 -->
615
+ <link name="d435_link"></link>
616
+ <joint name="d435_joint" type="fixed">
617
+ <origin xyz="0.0576235 0.01753 0.41987" rpy="0 0.8307767239493009 0"/>
618
+ <parent link="torso_link"/>
619
+ <child link="d435_link"/>
620
+ </joint>
621
+
622
+ <!-- mid360 -->
623
+ <link name="mid360_link"></link>
624
+ <joint name="mid360_joint" type="fixed">
625
+ <origin xyz="0.0002835 0.00003 0.40618" rpy="0 0.04014257279586953 0"/>
626
+ <parent link="torso_link"/>
627
+ <child link="mid360_link"/>
628
+ </joint>
629
+
630
+ <!-- Arm -->
631
+ <link name="left_shoulder_pitch_link">
632
+ <inertial>
633
+ <origin xyz="0 0.035892 -0.011628" rpy="0 0 0"/>
634
+ <mass value="0.718"/>
635
+ <inertia ixx="0.0004291" ixy="-9.2E-06" ixz="6.4E-06" iyy="0.000453" iyz="2.26E-05" izz="0.000423"/>
636
+ </inertial>
637
+ <visual>
638
+ <origin xyz="0 0 0" rpy="0 0 0"/>
639
+ <geometry>
640
+ <mesh filename="meshes/left_shoulder_pitch_link.STL"/>
641
+ </geometry>
642
+ <material name="white">
643
+ <color rgba="0.7 0.7 0.7 1"/>
644
+ </material>
645
+ </visual>
646
+ <collision>
647
+ <origin xyz="0 0.04 -0.01" rpy="0 1.5707963267948966 0"/>
648
+ <geometry>
649
+ <cylinder radius="0.03" length="0.05"/>
650
+ </geometry>
651
+ </collision>
652
+ </link>
653
+ <joint name="left_shoulder_pitch_joint" type="revolute">
654
+ <origin xyz="0.0039563 0.10022 0.23778" rpy="0.27931 5.4949E-05 -0.00019159"/>
655
+ <parent link="torso_link"/>
656
+ <child link="left_shoulder_pitch_link"/>
657
+ <axis xyz="0 1 0"/>
658
+ <limit lower="-3.0892" upper="2.6704" effort="25" velocity="37"/>
659
+ </joint>
660
+ <link name="left_shoulder_roll_link">
661
+ <inertial>
662
+ <origin xyz="-0.000227 0.00727 -0.063243" rpy="0 0 0"/>
663
+ <mass value="0.643"/>
664
+ <inertia ixx="0.0006177" ixy="-1E-06" ixz="8.7E-06" iyy="0.0006912" iyz="-5.3E-06" izz="0.0003894"/>
665
+ </inertial>
666
+ <visual>
667
+ <origin xyz="0 0 0" rpy="0 0 0"/>
668
+ <geometry>
669
+ <mesh filename="meshes/left_shoulder_roll_link.STL"/>
670
+ </geometry>
671
+ <material name="white">
672
+ <color rgba="0.7 0.7 0.7 1"/>
673
+ </material>
674
+ </visual>
675
+ <collision>
676
+ <origin xyz="-0.004 0.006 -0.053" rpy="0 0 0"/>
677
+ <geometry>
678
+ <cylinder radius="0.03" length="0.03"/>
679
+ </geometry>
680
+ </collision>
681
+ </link>
682
+ <joint name="left_shoulder_roll_joint" type="revolute">
683
+ <origin xyz="0 0.038 -0.013831" rpy="-0.27925 0 0"/>
684
+ <parent link="left_shoulder_pitch_link"/>
685
+ <child link="left_shoulder_roll_link"/>
686
+ <axis xyz="1 0 0"/>
687
+ <limit lower="-1.5882" upper="2.2515" effort="25" velocity="37"/>
688
+ </joint>
689
+ <link name="left_shoulder_yaw_link">
690
+ <inertial>
691
+ <origin xyz="0.010773 -0.002949 -0.072009" rpy="0 0 0"/>
692
+ <mass value="0.734"/>
693
+ <inertia ixx="0.0009988" ixy="7.9E-06" ixz="0.0001412" iyy="0.0010605" iyz="-2.86E-05" izz="0.0004354"/>
694
+ </inertial>
695
+ <visual>
696
+ <origin xyz="0 0 0" rpy="0 0 0"/>
697
+ <geometry>
698
+ <mesh filename="meshes/left_shoulder_yaw_link.STL"/>
699
+ </geometry>
700
+ <material name="white">
701
+ <color rgba="0.7 0.7 0.7 1"/>
702
+ </material>
703
+ </visual>
704
+ <collision>
705
+ <origin xyz="0 0 0" rpy="0 0 0"/>
706
+ <geometry>
707
+ <mesh filename="meshes/left_shoulder_yaw_link.STL"/>
708
+ </geometry>
709
+ </collision>
710
+ </link>
711
+ <joint name="left_shoulder_yaw_joint" type="revolute">
712
+ <origin xyz="0 0.00624 -0.1032" rpy="0 0 0"/>
713
+ <parent link="left_shoulder_roll_link"/>
714
+ <child link="left_shoulder_yaw_link"/>
715
+ <axis xyz="0 0 1"/>
716
+ <limit lower="-2.618" upper="2.618" effort="25" velocity="37"/>
717
+ </joint>
718
+ <link name="left_elbow_link">
719
+ <inertial>
720
+ <origin xyz="0.064956 0.004454 -0.010062" rpy="0 0 0"/>
721
+ <mass value="0.6"/>
722
+ <inertia ixx="0.0002891" ixy="6.53E-05" ixz="1.72E-05" iyy="0.0004152" iyz="-5.6E-06" izz="0.0004197"/>
723
+ </inertial>
724
+ <visual>
725
+ <origin xyz="0 0 0" rpy="0 0 0"/>
726
+ <geometry>
727
+ <mesh filename="meshes/left_elbow_link.STL"/>
728
+ </geometry>
729
+ <material name="white">
730
+ <color rgba="0.7 0.7 0.7 1"/>
731
+ </material>
732
+ </visual>
733
+ <collision>
734
+ <origin xyz="0 0 0" rpy="0 0 0"/>
735
+ <geometry>
736
+ <mesh filename="meshes/left_elbow_link.STL"/>
737
+ </geometry>
738
+ </collision>
739
+ </link>
740
+ <joint name="left_elbow_joint" type="revolute">
741
+ <origin xyz="0.015783 0 -0.080518" rpy="0 0 0"/>
742
+ <parent link="left_shoulder_yaw_link"/>
743
+ <child link="left_elbow_link"/>
744
+ <axis xyz="0 1 0"/>
745
+ <limit lower="-1.0472" upper="2.0944" effort="25" velocity="37"/>
746
+ </joint>
747
+ <joint name="left_wrist_roll_joint" type="revolute">
748
+ <origin xyz="0.100 0.00188791 -0.010" rpy="0 0 0"/>
749
+ <axis xyz="1 0 0"/>
750
+ <parent link="left_elbow_link"/>
751
+ <child link="left_wrist_roll_link"/>
752
+ <limit effort="25" velocity="37" lower="-1.972222054" upper="1.972222054"/>
753
+ </joint>
754
+ <link name="left_wrist_roll_link">
755
+ <inertial>
756
+ <origin xyz="0.01713944778 0.00053759094 0.00000048864" rpy="0 0 0"/>
757
+ <mass value="0.08544498"/>
758
+ <inertia ixx="0.00004821544023" ixy="-0.00000424511021" ixz="0.00000000510599" iyy="0.00003722899093" iyz="-0.00000000123525" izz="0.00005482106541"/>
759
+ </inertial>
760
+ <visual>
761
+ <origin xyz="0 0 0" rpy="0 0 0"/>
762
+ <geometry>
763
+ <mesh filename="meshes/left_wrist_roll_link.STL"/>
764
+ </geometry>
765
+ <material name="white">
766
+ <color rgba="0.7 0.7 0.7 1"/>
767
+ </material>
768
+ </visual>
769
+ <collision>
770
+ <origin xyz="0 0 0" rpy="0 0 0"/>
771
+ <geometry>
772
+ <mesh filename="meshes/left_wrist_roll_link.STL"/>
773
+ </geometry>
774
+ </collision>
775
+ </link>
776
+ <joint name="left_wrist_pitch_joint" type="revolute">
777
+ <origin xyz="0.038 0 0" rpy="0 0 0"/>
778
+ <axis xyz="0 1 0"/>
779
+ <parent link="left_wrist_roll_link"/>
780
+ <child link="left_wrist_pitch_link"/>
781
+ <limit effort="5" velocity="22" lower="-1.614429558" upper="1.614429558"/>
782
+ </joint>
783
+ <link name="left_wrist_pitch_link">
784
+ <inertial>
785
+ <origin xyz="0.02299989837 -0.00111685314 -0.00111658096" rpy="0 0 0"/>
786
+ <mass value="0.48404956"/>
787
+ <inertia ixx="0.00016579646273" ixy="-0.00001231206746" ixz="0.00001231699194" iyy="0.00042954057410" iyz="0.00000081417712" izz="0.00042953697654"/>
788
+ </inertial>
789
+ <visual>
790
+ <origin xyz="0 0 0" rpy="0 0 0"/>
791
+ <geometry>
792
+ <mesh filename="meshes/left_wrist_pitch_link.STL"/>
793
+ </geometry>
794
+ <material name="white">
795
+ <color rgba="0.7 0.7 0.7 1"/>
796
+ </material>
797
+ </visual>
798
+ <collision>
799
+ <origin xyz="0 0 0" rpy="0 0 0"/>
800
+ <geometry>
801
+ <mesh filename="meshes/left_wrist_pitch_link.STL"/>
802
+ </geometry>
803
+ </collision>
804
+ </link>
805
+ <joint name="left_wrist_yaw_joint" type="revolute">
806
+ <origin xyz="0.046 0 0" rpy="0 0 0"/>
807
+ <axis xyz="0 0 1"/>
808
+ <parent link="left_wrist_pitch_link"/>
809
+ <child link="left_wrist_yaw_link"/>
810
+ <limit effort="5" velocity="22" lower="-1.614429558" upper="1.614429558"/>
811
+ </joint>
812
+ <link name="left_wrist_yaw_link">
813
+ <inertial>
814
+ <origin xyz="0.02200381568 0.00049485096 0.00053861123" rpy="0 0 0"/>
815
+ <mass value="0.08457647"/>
816
+ <inertia ixx="0.00004929128828" ixy="-0.00000045735494" ixz="0.00000445867591" iyy="0.00005973338134" iyz="0.00000043217198" izz="0.00003928083826"/>
817
+ </inertial>
818
+ <visual>
819
+ <origin xyz="0 0 0" rpy="0 0 0"/>
820
+ <geometry>
821
+ <mesh filename="meshes/left_wrist_yaw_link.STL"/>
822
+ </geometry>
823
+ <material name="white">
824
+ <color rgba="0.7 0.7 0.7 1"/>
825
+ </material>
826
+ </visual>
827
+ <collision>
828
+ <origin xyz="0 0 0" rpy="0 0 0"/>
829
+ <geometry>
830
+ <mesh filename="meshes/left_wrist_yaw_link.STL"/>
831
+ </geometry>
832
+ </collision>
833
+ </link>
834
+ <joint name="left_hand_palm_joint" type="fixed">
835
+ <origin xyz="0.0415 0.003 0" rpy="0 0 0"/>
836
+ <parent link="left_wrist_yaw_link"/>
837
+ <child link="left_hand_palm_link"/>
838
+ </joint>
839
+ <link name="left_hand_palm_link">
840
+ <inertial>
841
+ <origin xyz="0.06214634836 -0.00050869656 -0.00058171093" rpy="0 0 0"/>
842
+ <mass value="0.37283854"/>
843
+ <inertia ixx="0.00027535181027" ixy="-0.00001595519465" ixz="-0.00000242161890" iyy="0.00053951827219" iyz="-0.00000042279435" izz="0.00039623390907"/>
844
+ </inertial>
845
+ <visual>
846
+ <origin xyz="0 0 0" rpy="0 0 0"/>
847
+ <geometry>
848
+ <mesh filename="meshes/left_hand_palm_link.STL"/>
849
+ </geometry>
850
+ <material name="white">
851
+ <color rgba="0.7 0.7 0.7 1"/>
852
+ </material>
853
+ </visual>
854
+ <collision>
855
+ <origin xyz="0 0 0" rpy="0 0 0"/>
856
+ <geometry>
857
+ <mesh filename="meshes/left_hand_palm_link.STL"/>
858
+ </geometry>
859
+ </collision>
860
+ </link>
861
+ <joint name="left_hand_thumb_0_joint" type="revolute">
862
+ <origin xyz="0.0255 0 0" rpy="0 0 0"/>
863
+ <axis xyz="0 1 0"/>
864
+ <parent link="left_hand_palm_link"/>
865
+ <child link="left_hand_thumb_0_link"/>
866
+ <limit effort="2.45" velocity="6.857" lower="-1.04719755" upper="1.04719755"/>
867
+ </joint>
868
+ <link name="left_hand_thumb_0_link">
869
+ <inertial>
870
+ <origin xyz="-0.00088424580 -0.00863407079 0.00094429336" rpy="0 0 0"/>
871
+ <mass value="0.08623657"/>
872
+ <inertia ixx="0.00001602919238" ixy="0.00000010683177" ixz="0.00000016728875" iyy="0.00001451795012" iyz="-0.00000051094752" izz="0.00001637877663"/>
873
+ </inertial>
874
+ <visual>
875
+ <origin xyz="0 0 0" rpy="0 0 0"/>
876
+ <geometry>
877
+ <mesh filename="meshes/left_hand_thumb_0_link.STL"/>
878
+ </geometry>
879
+ <material name="white">
880
+ <color rgba="0.7 0.7 0.7 1"/>
881
+ </material>
882
+ </visual>
883
+ <collision>
884
+ <origin xyz="0 0 0" rpy="0 0 0"/>
885
+ <geometry>
886
+ <mesh filename="meshes/left_hand_thumb_0_link.STL"/>
887
+ </geometry>
888
+ </collision>
889
+ </link>
890
+ <joint name="left_hand_thumb_1_joint" type="revolute">
891
+ <origin xyz="-0.0025 -0.0193 0" rpy="0 0 0"/>
892
+ <axis xyz="0 0 1"/>
893
+ <parent link="left_hand_thumb_0_link"/>
894
+ <child link="left_hand_thumb_1_link"/>
895
+ <limit effort="1.4" velocity="12" lower="-0.72431163" upper="1.04719755"/>
896
+ </joint>
897
+ <link name="left_hand_thumb_1_link">
898
+ <inertial>
899
+ <origin xyz="-0.00082788768 -0.03547435774 -0.00038089960" rpy="0 0 0"/>
900
+ <mass value="0.05885070"/>
901
+ <inertia ixx="0.00001274699945" ixy="-0.00000050770784" ixz="0.00000016088850" iyy="0.00000601573947" iyz="-0.00000027839003" izz="0.00001234543582"/>
902
+ </inertial>
903
+ <visual>
904
+ <origin xyz="0 0 0" rpy="0 0 0"/>
905
+ <geometry>
906
+ <mesh filename="meshes/left_hand_thumb_1_link.STL"/>
907
+ </geometry>
908
+ <material name="white">
909
+ <color rgba="0.7 0.7 0.7 1"/>
910
+ </material>
911
+ </visual>
912
+ <collision>
913
+ <origin xyz="-0.001 -0.032 0" rpy="0 0 0"/>
914
+ <geometry>
915
+ <box size="0.02 0.03 0.02"/>
916
+ </geometry>
917
+ </collision>
918
+ </link>
919
+ <joint name="left_hand_thumb_2_joint" type="revolute">
920
+ <origin xyz="0 -0.0458 0" rpy="0 0 0"/>
921
+ <axis xyz="0 0 1"/>
922
+ <parent link="left_hand_thumb_1_link"/>
923
+ <child link="left_hand_thumb_2_link"/>
924
+ <limit effort="1.4" velocity="12" lower="0" upper="1.74532925"/>
925
+ </joint>
926
+ <link name="left_hand_thumb_2_link">
927
+ <inertial>
928
+ <origin xyz="-0.00171735242 -0.02628192939 0.00010778879" rpy="0 0 0"/>
929
+ <mass value="0.02030626"/>
930
+ <inertia ixx="0.00000461267817" ixy="-0.00000003422130" ixz="-0.00000000823881" iyy="0.00000153561368" iyz="-0.00000002549885" izz="0.00000386625776"/>
931
+ </inertial>
932
+ <visual>
933
+ <origin xyz="0 0 0" rpy="0 0 0"/>
934
+ <geometry>
935
+ <mesh filename="meshes/left_hand_thumb_2_link.STL"/>
936
+ </geometry>
937
+ <material name="white">
938
+ <color rgba="0.7 0.7 0.7 1"/>
939
+ </material>
940
+ </visual>
941
+ <collision>
942
+ <origin xyz="0 0 0" rpy="0 0 0"/>
943
+ <geometry>
944
+ <mesh filename="meshes/left_hand_thumb_2_link.STL"/>
945
+ </geometry>
946
+ </collision>
947
+ </link>
948
+ <joint name="left_hand_middle_0_joint" type="revolute">
949
+ <origin xyz="0.0777 0.0016 -0.0285" rpy="0 0 0"/>
950
+ <axis xyz="0 0 1"/>
951
+ <parent link="left_hand_palm_link"/>
952
+ <child link="left_hand_middle_0_link"/>
953
+ <limit effort="1.4" velocity="12" lower="-1.57079632" upper="0"/>
954
+ </joint>
955
+ <link name="left_hand_middle_0_link">
956
+ <inertial>
957
+ <origin xyz="0.03547435774 0.00082788768 0.00038089960" rpy="0 0 0"/>
958
+ <mass value="0.05885070"/>
959
+ <inertia ixx="0.00000601573947" ixy="-0.00000050770784" ixz="-0.00000027839003" iyy="0.00001274699945" iyz="0.00000016088850" izz="0.00001234543582"/>
960
+ </inertial>
961
+ <visual>
962
+ <origin xyz="0 0 0" rpy="0 0 0"/>
963
+ <geometry>
964
+ <mesh filename="meshes/left_hand_middle_0_link.STL"/>
965
+ </geometry>
966
+ <material name="white">
967
+ <color rgba="0.7 0.7 0.7 1"/>
968
+ </material>
969
+ </visual>
970
+ <collision>
971
+ <origin xyz="0 0 0" rpy="0 0 0"/>
972
+ <geometry>
973
+ <mesh filename="meshes/left_hand_middle_0_link.STL"/>
974
+ </geometry>
975
+ </collision>
976
+ </link>
977
+ <joint name="left_hand_middle_1_joint" type="revolute">
978
+ <origin xyz="0.0458 0 0" rpy="0 0 0"/>
979
+ <axis xyz="0 0 1"/>
980
+ <parent link="left_hand_middle_0_link"/>
981
+ <child link="left_hand_middle_1_link"/>
982
+ <limit effort="1.4" velocity="12" lower="-1.74532925" upper="0"/>
983
+ </joint>
984
+ <link name="left_hand_middle_1_link">
985
+ <inertial>
986
+ <origin xyz="0.02628192939 0.00171735242 -0.00010778879" rpy="0 0 0"/>
987
+ <mass value="0.02030626"/>
988
+ <inertia ixx="0.00000153561368" ixy="-0.00000003422130" ixz="-0.00000002549885" iyy="0.00000461267817" iyz="-0.00000000823881" izz="0.00000386625776"/>
989
+ </inertial>
990
+ <visual>
991
+ <origin xyz="0 0 0" rpy="0 0 0"/>
992
+ <geometry>
993
+ <mesh filename="meshes/left_hand_middle_1_link.STL"/>
994
+ </geometry>
995
+ <material name="white">
996
+ <color rgba="0.7 0.7 0.7 1"/>
997
+ </material>
998
+ </visual>
999
+ <collision>
1000
+ <origin xyz="0 0 0" rpy="0 0 0"/>
1001
+ <geometry>
1002
+ <mesh filename="meshes/left_hand_middle_1_link.STL"/>
1003
+ </geometry>
1004
+ </collision>
1005
+ </link>
1006
+ <joint name="left_hand_index_0_joint" type="revolute">
1007
+ <origin xyz="0.0777 0.0016 0.0285" rpy="0 0 0"/>
1008
+ <axis xyz="0 0 1"/>
1009
+ <parent link="left_hand_palm_link"/>
1010
+ <child link="left_hand_index_0_link"/>
1011
+ <limit effort="1.4" velocity="12" lower="-1.57079632" upper="0"/>
1012
+ </joint>
1013
+ <link name="left_hand_index_0_link">
1014
+ <inertial>
1015
+ <origin xyz="0.03547435774 0.00082788768 0.00038089960" rpy="0 0 0"/>
1016
+ <mass value="0.05885070"/>
1017
+ <inertia ixx="0.00000601573947" ixy="-0.00000050770784" ixz="-0.00000027839003" iyy="0.00001274699945" iyz="0.00000016088850" izz="0.00001234543582"/>
1018
+ </inertial>
1019
+ <visual>
1020
+ <origin xyz="0 0 0" rpy="0 0 0"/>
1021
+ <geometry>
1022
+ <mesh filename="meshes/left_hand_index_0_link.STL"/>
1023
+ </geometry>
1024
+ <material name="white">
1025
+ <color rgba="0.7 0.7 0.7 1"/>
1026
+ </material>
1027
+ </visual>
1028
+ <collision>
1029
+ <origin xyz="0 0 0" rpy="0 0 0"/>
1030
+ <geometry>
1031
+ <mesh filename="meshes/left_hand_index_0_link.STL"/>
1032
+ </geometry>
1033
+ </collision>
1034
+ </link>
1035
+ <joint name="left_hand_index_1_joint" type="revolute">
1036
+ <origin xyz="0.0458 0 0" rpy="0 0 0"/>
1037
+ <axis xyz="0 0 1"/>
1038
+ <parent link="left_hand_index_0_link"/>
1039
+ <child link="left_hand_index_1_link"/>
1040
+ <limit effort="1.4" velocity="12" lower="-1.74532925" upper="0"/>
1041
+ </joint>
1042
+ <link name="left_hand_index_1_link">
1043
+ <inertial>
1044
+ <origin xyz="0.02628192939 0.00171735242 -0.00010778879" rpy="0 0 0"/>
1045
+ <mass value="0.02030626"/>
1046
+ <inertia ixx="0.00000153561368" ixy="-0.00000003422130" ixz="-0.00000002549885" iyy="0.00000461267817" iyz="-0.00000000823881" izz="0.00000386625776"/>
1047
+ </inertial>
1048
+ <visual>
1049
+ <origin xyz="0 0 0" rpy="0 0 0"/>
1050
+ <geometry>
1051
+ <mesh filename="meshes/left_hand_index_1_link.STL"/>
1052
+ </geometry>
1053
+ <material name="white">
1054
+ <color rgba="0.7 0.7 0.7 1"/>
1055
+ </material>
1056
+ </visual>
1057
+ <collision>
1058
+ <origin xyz="0 0 0" rpy="0 0 0"/>
1059
+ <geometry>
1060
+ <mesh filename="meshes/left_hand_index_1_link.STL"/>
1061
+ </geometry>
1062
+ </collision>
1063
+ </link>
1064
+ <link name="right_shoulder_pitch_link">
1065
+ <inertial>
1066
+ <origin xyz="0 -0.035892 -0.011628" rpy="0 0 0"/>
1067
+ <mass value="0.718"/>
1068
+ <inertia ixx="0.0004291" ixy="9.2E-06" ixz="6.4E-06" iyy="0.000453" iyz="-2.26E-05" izz="0.000423"/>
1069
+ </inertial>
1070
+ <visual>
1071
+ <origin xyz="0 0 0" rpy="0 0 0"/>
1072
+ <geometry>
1073
+ <mesh filename="meshes/right_shoulder_pitch_link.STL"/>
1074
+ </geometry>
1075
+ <material name="white">
1076
+ <color rgba="0.7 0.7 0.7 1"/>
1077
+ </material>
1078
+ </visual>
1079
+ <collision>
1080
+ <origin xyz="0 -0.04 -0.01" rpy="0 1.5707963267948966 0"/>
1081
+ <geometry>
1082
+ <cylinder radius="0.03" length="0.05"/>
1083
+ </geometry>
1084
+ </collision>
1085
+ </link>
1086
+ <joint name="right_shoulder_pitch_joint" type="revolute">
1087
+ <origin xyz="0.0039563 -0.10021 0.23778" rpy="-0.27931 5.4949E-05 0.00019159"/>
1088
+ <parent link="torso_link"/>
1089
+ <child link="right_shoulder_pitch_link"/>
1090
+ <axis xyz="0 1 0"/>
1091
+ <limit lower="-3.0892" upper="2.6704" effort="25" velocity="37"/>
1092
+ </joint>
1093
+ <link name="right_shoulder_roll_link">
1094
+ <inertial>
1095
+ <origin xyz="-0.000227 -0.00727 -0.063243" rpy="0 0 0"/>
1096
+ <mass value="0.643"/>
1097
+ <inertia ixx="0.0006177" ixy="1E-06" ixz="8.7E-06" iyy="0.0006912" iyz="5.3E-06" izz="0.0003894"/>
1098
+ </inertial>
1099
+ <visual>
1100
+ <origin xyz="0 0 0" rpy="0 0 0"/>
1101
+ <geometry>
1102
+ <mesh filename="meshes/right_shoulder_roll_link.STL"/>
1103
+ </geometry>
1104
+ <material name="white">
1105
+ <color rgba="0.7 0.7 0.7 1"/>
1106
+ </material>
1107
+ </visual>
1108
+ <collision>
1109
+ <origin xyz="-0.004 -0.006 -0.053" rpy="0 0 0"/>
1110
+ <geometry>
1111
+ <cylinder radius="0.03" length="0.03"/>
1112
+ </geometry>
1113
+ </collision>
1114
+ </link>
1115
+ <joint name="right_shoulder_roll_joint" type="revolute">
1116
+ <origin xyz="0 -0.038 -0.013831" rpy="0.27925 0 0"/>
1117
+ <parent link="right_shoulder_pitch_link"/>
1118
+ <child link="right_shoulder_roll_link"/>
1119
+ <axis xyz="1 0 0"/>
1120
+ <limit lower="-2.2515" upper="1.5882" effort="25" velocity="37"/>
1121
+ </joint>
1122
+ <link name="right_shoulder_yaw_link">
1123
+ <inertial>
1124
+ <origin xyz="0.010773 0.002949 -0.072009" rpy="0 0 0"/>
1125
+ <mass value="0.734"/>
1126
+ <inertia ixx="0.0009988" ixy="-7.9E-06" ixz="0.0001412" iyy="0.0010605" iyz="2.86E-05" izz="0.0004354"/>
1127
+ </inertial>
1128
+ <visual>
1129
+ <origin xyz="0 0 0" rpy="0 0 0"/>
1130
+ <geometry>
1131
+ <mesh filename="meshes/right_shoulder_yaw_link.STL"/>
1132
+ </geometry>
1133
+ <material name="white">
1134
+ <color rgba="0.7 0.7 0.7 1"/>
1135
+ </material>
1136
+ </visual>
1137
+ <collision>
1138
+ <origin xyz="0 0 0" rpy="0 0 0"/>
1139
+ <geometry>
1140
+ <mesh filename="meshes/right_shoulder_yaw_link.STL"/>
1141
+ </geometry>
1142
+ </collision>
1143
+ </link>
1144
+ <joint name="right_shoulder_yaw_joint" type="revolute">
1145
+ <origin xyz="0 -0.00624 -0.1032" rpy="0 0 0"/>
1146
+ <parent link="right_shoulder_roll_link"/>
1147
+ <child link="right_shoulder_yaw_link"/>
1148
+ <axis xyz="0 0 1"/>
1149
+ <limit lower="-2.618" upper="2.618" effort="25" velocity="37"/>
1150
+ </joint>
1151
+ <link name="right_elbow_link">
1152
+ <inertial>
1153
+ <origin xyz="0.064956 -0.004454 -0.010062" rpy="0 0 0"/>
1154
+ <mass value="0.6"/>
1155
+ <inertia ixx="0.0002891" ixy="-6.53E-05" ixz="1.72E-05" iyy="0.0004152" iyz="5.6E-06" izz="0.0004197"/>
1156
+ </inertial>
1157
+ <visual>
1158
+ <origin xyz="0 0 0" rpy="0 0 0"/>
1159
+ <geometry>
1160
+ <mesh filename="meshes/right_elbow_link.STL"/>
1161
+ </geometry>
1162
+ <material name="white">
1163
+ <color rgba="0.7 0.7 0.7 1"/>
1164
+ </material>
1165
+ </visual>
1166
+ <collision>
1167
+ <origin xyz="0 0 0" rpy="0 0 0"/>
1168
+ <geometry>
1169
+ <mesh filename="meshes/right_elbow_link.STL"/>
1170
+ </geometry>
1171
+ </collision>
1172
+ </link>
1173
+ <joint name="right_elbow_joint" type="revolute">
1174
+ <origin xyz="0.015783 0 -0.080518" rpy="0 0 0"/>
1175
+ <parent link="right_shoulder_yaw_link"/>
1176
+ <child link="right_elbow_link"/>
1177
+ <axis xyz="0 1 0"/>
1178
+ <limit lower="-1.0472" upper="2.0944" effort="25" velocity="37"/>
1179
+ </joint>
1180
+ <joint name="right_wrist_roll_joint" type="revolute">
1181
+ <origin xyz="0.100 -0.00188791 -0.010" rpy="0 0 0"/>
1182
+ <axis xyz="1 0 0"/>
1183
+ <parent link="right_elbow_link"/>
1184
+ <child link="right_wrist_roll_link"/>
1185
+ <limit effort="25" velocity="37" lower="-1.972222054" upper="1.972222054"/>
1186
+ </joint>
1187
+ <link name="right_wrist_roll_link">
1188
+ <inertial>
1189
+ <origin xyz="0.01713944778 -0.00053759094 0.00000048864" rpy="0 0 0"/>
1190
+ <mass value="0.08544498"/>
1191
+ <inertia ixx="0.00004821544023" ixy="0.00000424511021" ixz="0.00000000510599" iyy="0.00003722899093" iyz="0.00000000123525" izz="0.00005482106541"/>
1192
+ </inertial>
1193
+ <visual>
1194
+ <origin xyz="0 0 0" rpy="0 0 0"/>
1195
+ <geometry>
1196
+ <mesh filename="meshes/right_wrist_roll_link.STL"/>
1197
+ </geometry>
1198
+ <material name="white">
1199
+ <color rgba="0.7 0.7 0.7 1"/>
1200
+ </material>
1201
+ </visual>
1202
+ <collision>
1203
+ <origin xyz="0 0 0" rpy="0 0 0"/>
1204
+ <geometry>
1205
+ <mesh filename="meshes/right_wrist_roll_link.STL"/>
1206
+ </geometry>
1207
+ </collision>
1208
+ </link>
1209
+ <joint name="right_wrist_pitch_joint" type="revolute">
1210
+ <origin xyz="0.038 0 0" rpy="0 0 0"/>
1211
+ <axis xyz="0 1 0"/>
1212
+ <parent link="right_wrist_roll_link"/>
1213
+ <child link="right_wrist_pitch_link"/>
1214
+ <limit effort="5" velocity="22" lower="-1.614429558" upper="1.614429558"/>
1215
+ </joint>
1216
+ <link name="right_wrist_pitch_link">
1217
+ <inertial>
1218
+ <origin xyz="0.02299989837 0.00111685314 -0.00111658096" rpy="0 0 0"/>
1219
+ <mass value="0.48404956"/>
1220
+ <inertia ixx="0.00016579646273" ixy="0.00001231206746" ixz="0.00001231699194" iyy="0.00042954057410" iyz="-0.00000081417712" izz="0.00042953697654"/>
1221
+ </inertial>
1222
+ <visual>
1223
+ <origin xyz="0 0 0" rpy="0 0 0"/>
1224
+ <geometry>
1225
+ <mesh filename="meshes/right_wrist_pitch_link.STL"/>
1226
+ </geometry>
1227
+ <material name="white">
1228
+ <color rgba="0.7 0.7 0.7 1"/>
1229
+ </material>
1230
+ </visual>
1231
+ <collision>
1232
+ <origin xyz="0 0 0" rpy="0 0 0"/>
1233
+ <geometry>
1234
+ <mesh filename="meshes/right_wrist_pitch_link.STL"/>
1235
+ </geometry>
1236
+ </collision>
1237
+ </link>
1238
+ <joint name="right_wrist_yaw_joint" type="revolute">
1239
+ <origin xyz="0.046 0 0" rpy="0 0 0"/>
1240
+ <axis xyz="0 0 1"/>
1241
+ <parent link="right_wrist_pitch_link"/>
1242
+ <child link="right_wrist_yaw_link"/>
1243
+ <limit effort="5" velocity="22" lower="-1.614429558" upper="1.614429558"/>
1244
+ </joint>
1245
+ <link name="right_wrist_yaw_link">
1246
+ <inertial>
1247
+ <origin xyz="0.02200381568 -0.00049485096 0.00053861123" rpy="0 0 0"/>
1248
+ <mass value="0.08457647"/>
1249
+ <inertia ixx="0.00004929128828" ixy="0.00000045735494" ixz="0.00000445867591" iyy="0.00005973338134" iyz="-0.00000043217198" izz="0.00003928083826"/>
1250
+ </inertial>
1251
+ <visual>
1252
+ <origin xyz="0 0 0" rpy="0 0 0"/>
1253
+ <geometry>
1254
+ <mesh filename="meshes/right_wrist_yaw_link.STL"/>
1255
+ </geometry>
1256
+ <material name="white">
1257
+ <color rgba="0.7 0.7 0.7 1"/>
1258
+ </material>
1259
+ </visual>
1260
+ <collision>
1261
+ <origin xyz="0 0 0" rpy="0 0 0"/>
1262
+ <geometry>
1263
+ <mesh filename="meshes/right_wrist_yaw_link.STL"/>
1264
+ </geometry>
1265
+ </collision>
1266
+ </link>
1267
+ <joint name="right_hand_palm_joint" type="fixed">
1268
+ <origin xyz="0.0415 -0.003 0" rpy="0 0 0"/>
1269
+ <parent link="right_wrist_yaw_link"/>
1270
+ <child link="right_hand_palm_link"/>
1271
+ </joint>
1272
+ <link name="right_hand_palm_link">
1273
+ <inertial>
1274
+ <origin xyz="0.06214634836 0.00050869656 -0.00058171093" rpy="0 0 0"/>
1275
+ <mass value="0.37283854"/>
1276
+ <inertia ixx="0.00027535181027" ixy="0.00001595519465" ixz="-0.00000242161890" iyy="0.00053951827219" iyz="0.00000042279435" izz="0.00039623390907"/>
1277
+ </inertial>
1278
+ <visual>
1279
+ <origin xyz="0 0 0" rpy="0 0 0"/>
1280
+ <geometry>
1281
+ <mesh filename="meshes/right_hand_palm_link.STL"/>
1282
+ </geometry>
1283
+ <material name="white">
1284
+ <color rgba="0.7 0.7 0.7 1"/>
1285
+ </material>
1286
+ </visual>
1287
+ <collision>
1288
+ <origin xyz="0 0 0" rpy="0 0 0"/>
1289
+ <geometry>
1290
+ <mesh filename="meshes/right_hand_palm_link.STL"/>
1291
+ </geometry>
1292
+ </collision>
1293
+ </link>
1294
+ <joint name="right_hand_thumb_0_joint" type="revolute">
1295
+ <origin xyz="0.0255 0 0" rpy="0 0 0"/>
1296
+ <axis xyz="0 1 0"/>
1297
+ <parent link="right_hand_palm_link"/>
1298
+ <child link="right_hand_thumb_0_link"/>
1299
+ <limit effort="2.45" velocity="6.857" lower="-1.04719755" upper="1.04719755"/>
1300
+ </joint>
1301
+ <link name="right_hand_thumb_0_link">
1302
+ <inertial>
1303
+ <origin xyz="-0.00088424580 0.00863407079 0.00094429336" rpy="0 0 0"/>
1304
+ <mass value="0.08623657"/>
1305
+ <inertia ixx="0.00001602919238" ixy="-0.00000010683177" ixz="0.00000016728875" iyy="0.00001451795012" iyz="0.00000051094752" izz="0.00001637877663"/>
1306
+ </inertial>
1307
+ <visual>
1308
+ <origin xyz="0 0 0" rpy="0 0 0"/>
1309
+ <geometry>
1310
+ <mesh filename="meshes/right_hand_thumb_0_link.STL"/>
1311
+ </geometry>
1312
+ <material name="white">
1313
+ <color rgba="0.7 0.7 0.7 1"/>
1314
+ </material>
1315
+ </visual>
1316
+ <collision>
1317
+ <origin xyz="0 0 0" rpy="0 0 0"/>
1318
+ <geometry>
1319
+ <mesh filename="meshes/right_hand_thumb_0_link.STL"/>
1320
+ </geometry>
1321
+ </collision>
1322
+ </link>
1323
+ <joint name="right_hand_thumb_1_joint" type="revolute">
1324
+ <origin xyz="-0.0025 0.0193 0" rpy="0 0 0"/>
1325
+ <axis xyz="0 0 1"/>
1326
+ <parent link="right_hand_thumb_0_link"/>
1327
+ <child link="right_hand_thumb_1_link"/>
1328
+ <limit effort="1.4" velocity="12" lower="-1.04719755" upper="0.72431163"/>
1329
+ </joint>
1330
+ <link name="right_hand_thumb_1_link">
1331
+ <inertial>
1332
+ <origin xyz="-0.00082788768 0.03547435774 -0.00038089960" rpy="0 0 0"/>
1333
+ <mass value="0.05885070"/>
1334
+ <inertia ixx="0.00001274699945" ixy="0.00000050770784" ixz="0.00000016088850" iyy="0.00000601573947" iyz="0.00000027839003" izz="0.00001234543582"/>
1335
+ </inertial>
1336
+ <visual>
1337
+ <origin xyz="0 0 0" rpy="0 0 0"/>
1338
+ <geometry>
1339
+ <mesh filename="meshes/right_hand_thumb_1_link.STL"/>
1340
+ </geometry>
1341
+ <material name="white">
1342
+ <color rgba="0.7 0.7 0.7 1"/>
1343
+ </material>
1344
+ </visual>
1345
+ <collision>
1346
+ <origin xyz="-0.001 0.032 0" rpy="0 0 0"/>
1347
+ <geometry>
1348
+ <box size="0.02 0.03 0.02"/>
1349
+ </geometry>
1350
+ </collision>
1351
+ </link>
1352
+ <joint name="right_hand_thumb_2_joint" type="revolute">
1353
+ <origin xyz="0 0.0458 0" rpy="0 0 0"/>
1354
+ <axis xyz="0 0 1"/>
1355
+ <parent link="right_hand_thumb_1_link"/>
1356
+ <child link="right_hand_thumb_2_link"/>
1357
+ <limit effort="1.4" velocity="12" lower="-1.74532925" upper="0"/>
1358
+ </joint>
1359
+ <link name="right_hand_thumb_2_link">
1360
+ <inertial>
1361
+ <origin xyz="-0.00171735242 0.02628192939 0.00010778879" rpy="0 0 0"/>
1362
+ <mass value="0.02030626"/>
1363
+ <inertia ixx="0.00000461267817" ixy="0.00000003422130" ixz="-0.00000000823881" iyy="0.00000153561368" iyz="0.00000002549885" izz="0.00000386625776"/>
1364
+ </inertial>
1365
+ <visual>
1366
+ <origin xyz="0 0 0" rpy="0 0 0"/>
1367
+ <geometry>
1368
+ <mesh filename="meshes/right_hand_thumb_2_link.STL"/>
1369
+ </geometry>
1370
+ <material name="white">
1371
+ <color rgba="0.7 0.7 0.7 1"/>
1372
+ </material>
1373
+ </visual>
1374
+ <collision>
1375
+ <origin xyz="0 0 0" rpy="0 0 0"/>
1376
+ <geometry>
1377
+ <mesh filename="meshes/right_hand_thumb_2_link.STL"/>
1378
+ </geometry>
1379
+ </collision>
1380
+ </link>
1381
+ <joint name="right_hand_middle_0_joint" type="revolute">
1382
+ <origin xyz="0.0777 -0.0016 -0.0285" rpy="0 0 0"/>
1383
+ <axis xyz="0 0 1"/>
1384
+ <parent link="right_hand_palm_link"/>
1385
+ <child link="right_hand_middle_0_link"/>
1386
+ <limit effort="1.4" velocity="12" lower="0" upper="1.57079632"/>
1387
+ </joint>
1388
+ <link name="right_hand_middle_0_link">
1389
+ <inertial>
1390
+ <origin xyz="0.03547435774 -0.00082788768 0.00038089960" rpy="0 0 0"/>
1391
+ <mass value="0.05885070"/>
1392
+ <inertia ixx="0.00000601573947" ixy="0.00000050770784" ixz="-0.00000027839003" iyy="0.00001274699945" iyz="-0.00000016088850" izz="0.00001234543582"/>
1393
+ </inertial>
1394
+ <visual>
1395
+ <origin xyz="0 0 0" rpy="0 0 0"/>
1396
+ <geometry>
1397
+ <mesh filename="meshes/right_hand_middle_0_link.STL"/>
1398
+ </geometry>
1399
+ <material name="white">
1400
+ <color rgba="0.7 0.7 0.7 1"/>
1401
+ </material>
1402
+ </visual>
1403
+ <collision>
1404
+ <origin xyz="0 0 0" rpy="0 0 0"/>
1405
+ <geometry>
1406
+ <mesh filename="meshes/right_hand_middle_0_link.STL"/>
1407
+ </geometry>
1408
+ </collision>
1409
+ </link>
1410
+ <joint name="right_hand_middle_1_joint" type="revolute">
1411
+ <origin xyz="0.0458 0 0" rpy="0 0 0"/>
1412
+ <axis xyz="0 0 1"/>
1413
+ <parent link="right_hand_middle_0_link"/>
1414
+ <child link="right_hand_middle_1_link"/>
1415
+ <limit effort="1.4" velocity="12" lower="0" upper="1.74532925"/>
1416
+ </joint>
1417
+ <link name="right_hand_middle_1_link">
1418
+ <inertial>
1419
+ <origin xyz="0.02628192939 -0.00171735242 -0.00010778879" rpy="0 0 0"/>
1420
+ <mass value="0.02030626"/>
1421
+ <inertia ixx="0.00000153561368" ixy="0.00000003422130" ixz="-0.00000002549885" iyy="0.00000461267817" iyz="0.00000000823881" izz="0.00000386625776"/>
1422
+ </inertial>
1423
+ <visual>
1424
+ <origin xyz="0 0 0" rpy="0 0 0"/>
1425
+ <geometry>
1426
+ <mesh filename="meshes/right_hand_middle_1_link.STL"/>
1427
+ </geometry>
1428
+ <material name="white">
1429
+ <color rgba="0.7 0.7 0.7 1"/>
1430
+ </material>
1431
+ </visual>
1432
+ <collision>
1433
+ <origin xyz="0 0 0" rpy="0 0 0"/>
1434
+ <geometry>
1435
+ <mesh filename="meshes/right_hand_middle_1_link.STL"/>
1436
+ </geometry>
1437
+ </collision>
1438
+ </link>
1439
+ <joint name="right_hand_index_0_joint" type="revolute">
1440
+ <origin xyz="0.0777 -0.0016 0.0285" rpy="0 0 0"/>
1441
+ <axis xyz="0 0 1"/>
1442
+ <parent link="right_hand_palm_link"/>
1443
+ <child link="right_hand_index_0_link"/>
1444
+ <limit effort="1.4" velocity="12" lower="0" upper="1.57079632"/>
1445
+ </joint>
1446
+ <link name="right_hand_index_0_link">
1447
+ <inertial>
1448
+ <origin xyz="0.03547435774 -0.00082788768 0.00038089960" rpy="0 0 0"/>
1449
+ <mass value="0.05885070"/>
1450
+ <inertia ixx="0.00000601573947" ixy="0.00000050770784" ixz="-0.00000027839003" iyy="0.00001274699945" iyz="-0.00000016088850" izz="0.00001234543582"/>
1451
+ </inertial>
1452
+ <visual>
1453
+ <origin xyz="0 0 0" rpy="0 0 0"/>
1454
+ <geometry>
1455
+ <mesh filename="meshes/right_hand_index_0_link.STL"/>
1456
+ </geometry>
1457
+ <material name="white">
1458
+ <color rgba="0.7 0.7 0.7 1"/>
1459
+ </material>
1460
+ </visual>
1461
+ <collision>
1462
+ <origin xyz="0 0 0" rpy="0 0 0"/>
1463
+ <geometry>
1464
+ <mesh filename="meshes/right_hand_index_0_link.STL"/>
1465
+ </geometry>
1466
+ </collision>
1467
+ </link>
1468
+ <joint name="right_hand_index_1_joint" type="revolute">
1469
+ <origin xyz="0.0458 0 0" rpy="0 0 0"/>
1470
+ <axis xyz="0 0 1"/>
1471
+ <parent link="right_hand_index_0_link"/>
1472
+ <child link="right_hand_index_1_link"/>
1473
+ <limit effort="1.4" velocity="12" lower="0" upper="1.74532925"/>
1474
+ </joint>
1475
+ <link name="right_hand_index_1_link">
1476
+ <inertial>
1477
+ <origin xyz="0.02628192939 -0.00171735242 -0.00010778879" rpy="0 0 0"/>
1478
+ <mass value="0.02030626"/>
1479
+ <inertia ixx="0.00000153561368" ixy="0.00000003422130" ixz="-0.00000002549885" iyy="0.00000461267817" iyz="0.00000000823881" izz="0.00000386625776"/>
1480
+ </inertial>
1481
+ <visual>
1482
+ <origin xyz="0 0 0" rpy="0 0 0"/>
1483
+ <geometry>
1484
+ <mesh filename="meshes/right_hand_index_1_link.STL"/>
1485
+ </geometry>
1486
+ <material name="white">
1487
+ <color rgba="0.7 0.7 0.7 1"/>
1488
+ </material>
1489
+ </visual>
1490
+ <collision>
1491
+ <origin xyz="0 0 0" rpy="0 0 0"/>
1492
+ <geometry>
1493
+ <mesh filename="meshes/right_hand_index_1_link.STL"/>
1494
+ </geometry>
1495
+ </collision>
1496
+ </link>
1497
+ </robot>
GR00T-WholeBodyControl/gear_sonic/data/robot_model/model_data/g1/g1_29dof_with_hand.xml ADDED
@@ -0,0 +1,751 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <mujoco model="g1_29dof_with_hand">
2
+ <compiler angle="radian" meshdir="meshes" />
3
+
4
+ <default>
5
+ <default class="torso_motor">
6
+ <joint damping="0.05" armature="0.01" frictionloss="0.2"/>
7
+ </default>
8
+ <default class="leg_motor">
9
+ <joint damping="0.05" armature="0.01" frictionloss="0.2"/>
10
+ </default>
11
+ <default class="ankle_motor">
12
+ <joint damping="0.05" armature="0.01" frictionloss="0.2"/>
13
+ </default>
14
+ <default class="arm_motor">
15
+ <joint damping="0.05" armature="0.01" frictionloss="0.2"/>
16
+ </default>
17
+ <default class="wrist_motor">
18
+ <joint damping="0.05" armature="0.01" frictionloss="0.1"/>
19
+ </default>
20
+ <default class="finger_motor">
21
+ <joint damping="0.05" armature="0.01" frictionloss="0.1"/>
22
+ </default>
23
+ </default>
24
+ <asset>
25
+ <mesh name="pelvis" file="pelvis.STL" />
26
+ <mesh name="pelvis_contour_link" file="pelvis_contour_link.STL" />
27
+ <mesh name="left_hip_pitch_link" file="left_hip_pitch_link.STL" />
28
+ <mesh name="left_hip_roll_link" file="left_hip_roll_link.STL" />
29
+ <mesh name="left_hip_yaw_link" file="left_hip_yaw_link.STL" />
30
+ <mesh name="left_knee_link" file="left_knee_link.STL" />
31
+ <mesh name="left_ankle_pitch_link" file="left_ankle_pitch_link.STL" />
32
+ <mesh name="left_ankle_roll_link" file="left_ankle_roll_link.STL" />
33
+ <mesh name="right_hip_pitch_link" file="right_hip_pitch_link.STL" />
34
+ <mesh name="right_hip_roll_link" file="right_hip_roll_link.STL" />
35
+ <mesh name="right_hip_yaw_link" file="right_hip_yaw_link.STL" />
36
+ <mesh name="right_knee_link" file="right_knee_link.STL" />
37
+ <mesh name="right_ankle_pitch_link" file="right_ankle_pitch_link.STL" />
38
+ <mesh name="right_ankle_roll_link" file="right_ankle_roll_link.STL" />
39
+ <mesh name="waist_yaw_link" file="waist_yaw_link.STL" />
40
+ <mesh name="waist_roll_link" file="waist_roll_link.STL" />
41
+ <mesh name="torso_link" file="torso_link.STL" />
42
+ <mesh name="logo_link" file="logo_link.STL" />
43
+ <mesh name="head_link" file="head_link.STL" />
44
+ <mesh name="waist_support_link" file="waist_support_link.STL" />
45
+ <mesh name="left_shoulder_pitch_link" file="left_shoulder_pitch_link.STL" />
46
+ <mesh name="left_shoulder_roll_link" file="left_shoulder_roll_link.STL" />
47
+ <mesh name="left_shoulder_yaw_link" file="left_shoulder_yaw_link.STL" />
48
+ <mesh name="left_elbow_link" file="left_elbow_link.STL" />
49
+ <mesh name="left_wrist_roll_link" file="left_wrist_roll_link.STL" />
50
+ <mesh name="left_wrist_pitch_link" file="left_wrist_pitch_link.STL" />
51
+ <mesh name="left_wrist_yaw_link" file="left_wrist_yaw_link.STL" />
52
+ <mesh name="left_hand_palm_link" file="left_hand_palm_link.STL" />
53
+ <mesh name="left_hand_thumb_0_link" file="left_hand_thumb_0_link.STL" />
54
+ <mesh name="left_hand_thumb_1_link" file="left_hand_thumb_1_link.STL" />
55
+ <mesh name="left_hand_thumb_2_link" file="left_hand_thumb_2_link.STL" />
56
+ <mesh name="left_hand_middle_0_link" file="left_hand_middle_0_link.STL" />
57
+ <mesh name="left_hand_middle_1_link" file="left_hand_middle_1_link.STL" />
58
+ <mesh name="left_hand_index_0_link" file="left_hand_index_0_link.STL" />
59
+ <mesh name="left_hand_index_1_link" file="left_hand_index_1_link.STL" />
60
+ <mesh name="right_shoulder_pitch_link" file="right_shoulder_pitch_link.STL" />
61
+ <mesh name="right_shoulder_roll_link" file="right_shoulder_roll_link.STL" />
62
+ <mesh name="right_shoulder_yaw_link" file="right_shoulder_yaw_link.STL" />
63
+ <mesh name="right_elbow_link" file="right_elbow_link.STL" />
64
+ <mesh name="right_wrist_roll_link" file="right_wrist_roll_link.STL" />
65
+ <mesh name="right_wrist_pitch_link" file="right_wrist_pitch_link.STL" />
66
+ <mesh name="right_wrist_yaw_link" file="right_wrist_yaw_link.STL" />
67
+ <mesh name="right_hand_palm_link" file="right_hand_palm_link.STL" />
68
+ <mesh name="right_hand_thumb_0_link" file="right_hand_thumb_0_link.STL" />
69
+ <mesh name="right_hand_thumb_1_link" file="right_hand_thumb_1_link.STL" />
70
+ <mesh name="right_hand_thumb_2_link" file="right_hand_thumb_2_link.STL" />
71
+ <mesh name="right_hand_middle_0_link" file="right_hand_middle_0_link.STL" />
72
+ <mesh name="right_hand_middle_1_link" file="right_hand_middle_1_link.STL" />
73
+ <mesh name="right_hand_index_0_link" file="right_hand_index_0_link.STL" />
74
+ <mesh name="right_hand_index_1_link" file="right_hand_index_1_link.STL" />
75
+ </asset>
76
+
77
+ <worldbody>
78
+ <body name="pelvis" pos="0 0 0.793">
79
+ <inertial pos="0 0 -0.07605" quat="1 0 -0.000399148 0" mass="3.813"
80
+ diaginertia="0.010549 0.0093089 0.0079184" />
81
+ <joint name="floating_base_joint" type="free" limited="false" actuatorfrclimited="false" />
82
+ <geom type="mesh" contype="0" conaffinity="0" group="1" density="0" rgba="0.2 0.2 0.2 1"
83
+ mesh="pelvis" />
84
+ <geom type="mesh" contype="0" conaffinity="0" group="1" density="0" rgba="0.7 0.7 0.7 1"
85
+ mesh="pelvis_contour_link" />
86
+ <geom type="mesh" rgba="0.7 0.7 0.7 1" mesh="pelvis_contour_link" />
87
+ <body name="left_hip_pitch_link" pos="0 0.064452 -0.1027">
88
+ <inertial pos="0.002741 0.047791 -0.02606" quat="0.954862 0.293964 0.0302556 0.030122"
89
+ mass="1.35" diaginertia="0.00181517 0.00153422 0.00116212" />
90
+ <joint name="left_hip_pitch_joint" pos="0 0 0" axis="0 1 0" range="-2.5307 2.8798"
91
+ actuatorfrcrange="-88 88" class="leg_motor" />
92
+ <geom type="mesh" contype="0" conaffinity="0" group="1" density="0" rgba="0.2 0.2 0.2 1"
93
+ mesh="left_hip_pitch_link" />
94
+ <geom type="mesh" rgba="0.2 0.2 0.2 1" mesh="left_hip_pitch_link" />
95
+ <body name="left_hip_roll_link" pos="0 0.052 -0.030465" quat="0.996179 0 -0.0873386 0">
96
+ <inertial pos="0.029812 -0.001045 -0.087934"
97
+ quat="0.977808 -1.97119e-05 0.205576 -0.0403793" mass="1.52"
98
+ diaginertia="0.00254986 0.00241169 0.00148755" />
99
+ <joint name="left_hip_roll_joint" pos="0 0 0" axis="1 0 0" range="-0.5236 2.9671"
100
+ actuatorfrcrange="-88 88" class="leg_motor" />
101
+ <geom type="mesh" contype="0" conaffinity="0" group="1" density="0" rgba="0.7 0.7 0.7 1"
102
+ mesh="left_hip_roll_link" />
103
+ <geom type="mesh" rgba="0.7 0.7 0.7 1" mesh="left_hip_roll_link" />
104
+ <body name="left_hip_yaw_link" pos="0.025001 0 -0.12412">
105
+ <inertial pos="-0.057709 -0.010981 -0.15078" quat="0.600598 0.15832 0.223482 0.751181"
106
+ mass="1.702" diaginertia="0.00776166 0.00717575 0.00160139" />
107
+ <joint name="left_hip_yaw_joint" pos="0 0 0" axis="0 0 1" range="-2.7576 2.7576"
108
+ actuatorfrcrange="-88 88" class="leg_motor" />
109
+ <geom type="mesh" contype="0" conaffinity="0" group="1" density="0" rgba="0.7 0.7 0.7 1"
110
+ mesh="left_hip_yaw_link" />
111
+ <geom type="mesh" rgba="0.7 0.7 0.7 1" mesh="left_hip_yaw_link" />
112
+ <body name="left_knee_link" pos="-0.078273 0.0021489 -0.17734"
113
+ quat="0.996179 0 0.0873386 0">
114
+ <inertial pos="0.005457 0.003964 -0.12074"
115
+ quat="0.923418 -0.0327699 0.0158246 0.382067" mass="1.932"
116
+ diaginertia="0.0113804 0.0112778 0.00146458" />
117
+ <joint name="left_knee_joint" pos="0 0 0" axis="0 1 0" range="-0.087267 2.8798"
118
+ actuatorfrcrange="-139 139" class="leg_motor" />
119
+ <geom type="mesh" contype="0" conaffinity="0" group="1" density="0"
120
+ rgba="0.7 0.7 0.7 1" mesh="left_knee_link" />
121
+ <geom type="mesh" rgba="0.7 0.7 0.7 1" mesh="left_knee_link" />
122
+ <body name="left_ankle_pitch_link" pos="0 -9.4445e-05 -0.30001">
123
+ <inertial pos="-0.007269 0 0.011137" quat="0.603053 0.369225 0.369225 0.603053"
124
+ mass="0.074" diaginertia="1.89e-05 1.40805e-05 6.9195e-06" />
125
+ <joint name="left_ankle_pitch_joint" pos="0 0 0" axis="0 1 0"
126
+ range="-0.87267 0.5236" actuatorfrcrange="-50 50" class="ankle_motor" />
127
+ <geom type="mesh" contype="0" conaffinity="0" group="1" density="0"
128
+ rgba="0.7 0.7 0.7 1" mesh="left_ankle_pitch_link" />
129
+ <geom type="mesh" rgba="0.7 0.7 0.7 1" mesh="left_ankle_pitch_link" />
130
+ <body name="left_ankle_roll_link" pos="0 0 -0.017558">
131
+ <inertial pos="0.026505 0 -0.016425"
132
+ quat="-0.000481092 0.728482 -0.000618967 0.685065" mass="0.608"
133
+ diaginertia="0.00167218 0.0016161 0.000217621" />
134
+ <joint name="left_ankle_roll_joint" pos="0 0 0" axis="1 0 0"
135
+ range="-0.2618 0.2618" actuatorfrcrange="-50 50" class="ankle_motor" />
136
+ <geom type="mesh" contype="0" conaffinity="0" group="1" density="0"
137
+ rgba="0.2 0.2 0.2 1" mesh="left_ankle_roll_link" />
138
+ <!-- Foot collision: box approximating the sole. Extents from
139
+ original 4-sphere corners: x ∈ [-0.05, 0.12], y ∈ ±0.03,
140
+ at z = -0.03. half-extents = (0.085, 0.03, 0.005). -->
141
+ <geom type="box" size="0.085 0.03 0.005" pos="0.035 0 -0.03"
142
+ rgba="0.2 0.2 0.2 0.4" />
143
+ </body>
144
+ </body>
145
+ </body>
146
+ </body>
147
+ </body>
148
+ </body>
149
+ <body name="right_hip_pitch_link" pos="0 -0.064452 -0.1027">
150
+ <inertial pos="0.002741 -0.047791 -0.02606" quat="0.954862 -0.293964 0.0302556 -0.030122"
151
+ mass="1.35" diaginertia="0.00181517 0.00153422 0.00116212" />
152
+ <joint name="right_hip_pitch_joint" pos="0 0 0" axis="0 1 0" range="-2.5307 2.8798"
153
+ actuatorfrcrange="-88 88" class="leg_motor" />
154
+ <geom type="mesh" contype="0" conaffinity="0" group="1" density="0" rgba="0.2 0.2 0.2 1"
155
+ mesh="right_hip_pitch_link" />
156
+ <geom type="mesh" rgba="0.2 0.2 0.2 1" mesh="right_hip_pitch_link" />
157
+ <body name="right_hip_roll_link" pos="0 -0.052 -0.030465" quat="0.996179 0 -0.0873386 0">
158
+ <inertial pos="0.029812 0.001045 -0.087934" quat="0.977808 1.97119e-05 0.205576 0.0403793"
159
+ mass="1.52" diaginertia="0.00254986 0.00241169 0.00148755" />
160
+ <joint name="right_hip_roll_joint" pos="0 0 0" axis="1 0 0" range="-2.9671 0.5236"
161
+ actuatorfrcrange="-88 88" class="leg_motor" />
162
+ <geom type="mesh" contype="0" conaffinity="0" group="1" density="0" rgba="0.7 0.7 0.7 1"
163
+ mesh="right_hip_roll_link" />
164
+ <geom type="mesh" rgba="0.7 0.7 0.7 1" mesh="right_hip_roll_link" />
165
+ <body name="right_hip_yaw_link" pos="0.025001 0 -0.12412">
166
+ <inertial pos="-0.057709 0.010981 -0.15078" quat="0.751181 0.223482 0.15832 0.600598"
167
+ mass="1.702" diaginertia="0.00776166 0.00717575 0.00160139" />
168
+ <joint name="right_hip_yaw_joint" pos="0 0 0" axis="0 0 1" range="-2.7576 2.7576"
169
+ actuatorfrcrange="-88 88" class="leg_motor" />
170
+ <geom type="mesh" contype="0" conaffinity="0" group="1" density="0" rgba="0.7 0.7 0.7 1"
171
+ mesh="right_hip_yaw_link" />
172
+ <geom type="mesh" rgba="0.7 0.7 0.7 1" mesh="right_hip_yaw_link" />
173
+ <body name="right_knee_link" pos="-0.078273 -0.0021489 -0.17734"
174
+ quat="0.996179 0 0.0873386 0">
175
+ <inertial pos="0.005457 -0.003964 -0.12074"
176
+ quat="0.923439 0.0345276 0.0116333 -0.382012" mass="1.932"
177
+ diaginertia="0.011374 0.0112843 0.00146452" />
178
+ <joint name="right_knee_joint" pos="0 0 0" axis="0 1 0" range="-0.087267 2.8798"
179
+ actuatorfrcrange="-139 139" class="leg_motor" />
180
+ <geom type="mesh" contype="0" conaffinity="0" group="1" density="0"
181
+ rgba="0.7 0.7 0.7 1" mesh="right_knee_link" />
182
+ <geom type="mesh" rgba="0.7 0.7 0.7 1" mesh="right_knee_link" />
183
+ <body name="right_ankle_pitch_link" pos="0 9.4445e-05 -0.30001">
184
+ <inertial pos="-0.007269 0 0.011137" quat="0.603053 0.369225 0.369225 0.603053"
185
+ mass="0.074" diaginertia="1.89e-05 1.40805e-05 6.9195e-06" />
186
+ <joint name="right_ankle_pitch_joint" pos="0 0 0" axis="0 1 0"
187
+ range="-0.87267 0.5236" actuatorfrcrange="-50 50" class="ankle_motor" />
188
+ <geom type="mesh" contype="0" conaffinity="0" group="1" density="0"
189
+ rgba="0.7 0.7 0.7 1" mesh="right_ankle_pitch_link" />
190
+ <geom type="mesh" rgba="0.7 0.7 0.7 1" mesh="right_ankle_pitch_link" />
191
+ <body name="right_ankle_roll_link" pos="0 0 -0.017558">
192
+ <inertial pos="0.026505 0 -0.016425"
193
+ quat="0.000481092 0.728482 0.000618967 0.685065" mass="0.608"
194
+ diaginertia="0.00167218 0.0016161 0.000217621" />
195
+ <joint name="right_ankle_roll_joint" pos="0 0 0" axis="1 0 0"
196
+ range="-0.2618 0.2618" actuatorfrcrange="-50 50" class="ankle_motor" />
197
+ <geom type="mesh" contype="0" conaffinity="0" group="1" density="0"
198
+ rgba="0.2 0.2 0.2 1" mesh="right_ankle_roll_link" />
199
+ <!-- Foot collision: box approximating the sole. Extents from
200
+ original 4-sphere corners: x ∈ [-0.05, 0.12], y ∈ ±0.03,
201
+ at z = -0.03. half-extents = (0.085, 0.03, 0.005). -->
202
+ <geom type="box" size="0.085 0.03 0.005" pos="0.035 0 -0.03"
203
+ rgba="0.2 0.2 0.2 0.4" />
204
+ </body>
205
+ </body>
206
+ </body>
207
+ </body>
208
+ </body>
209
+ </body>
210
+ <body name="waist_yaw_link">
211
+ <inertial pos="0.003964 0 0.018769" quat="-0.0178291 0.628464 0.0282471 0.777121"
212
+ mass="0.244" diaginertia="0.000158561 0.000124229 9.67669e-05" />
213
+ <joint name="waist_yaw_joint" pos="0 0 0" axis="0 0 1" range="-2.618 2.618"
214
+ actuatorfrcrange="-88 88" class="torso_motor" />
215
+ <geom type="mesh" contype="0" conaffinity="0" group="1" density="0" rgba="0.7 0.7 0.7 1"
216
+ mesh="waist_yaw_link" />
217
+ <body name="waist_roll_link" pos="-0.0039635 0 0.035">
218
+ <inertial pos="0 -0.000236 0.010111" quat="0.99979 0.020492 0 0" mass="0.047"
219
+ diaginertia="7.515e-06 6.40206e-06 3.98394e-06" />
220
+ <joint name="waist_roll_joint" pos="0 0 0" axis="1 0 0" range="-0.52 0.52"
221
+ actuatorfrcrange="-50 50" class="torso_motor"/>
222
+ <geom type="mesh" contype="0" conaffinity="0" group="1" density="0" rgba="0.7 0.7 0.7 1"
223
+ mesh="waist_roll_link" />
224
+ <body name="torso_link" pos="0 0 0.019">
225
+ <camera name="head_camera" pos="0.06 0.0 0.45" euler="0 -0.8 -1.57"/>
226
+ <inertial pos="0.00331658 0.000261533 0.179856"
227
+ quat="0.999831 0.000376204 0.0179895 -0.00377704" mass="9.598"
228
+ diaginertia="0.12407 0.111951 0.0325382" />
229
+ <joint name="waist_pitch_joint" pos="0 0 0" axis="0 1 0" range="-0.52 0.52"
230
+ actuatorfrcrange="-50 50" class="torso_motor"/>
231
+ <geom type="mesh" contype="0" conaffinity="0" group="1" density="0" rgba="0.7 0.7 0.7 1"
232
+ mesh="torso_link" />
233
+ <geom type="mesh" rgba="0.7 0.7 0.7 1" mesh="torso_link" />
234
+ <geom pos="0.0039635 0 -0.054" quat="1 0 0 0" type="mesh" contype="0" conaffinity="0"
235
+ group="1" density="0" rgba="0.2 0.2 0.2 1" mesh="logo_link" />
236
+ <geom pos="0.0039635 0 -0.054" quat="1 0 0 0" type="mesh" rgba="0.2 0.2 0.2 1"
237
+ mesh="logo_link" />
238
+ <geom pos="0.0039635 0 -0.054" type="mesh" contype="0" conaffinity="0" group="1"
239
+ density="0" rgba="0.2 0.2 0.2 1" mesh="head_link" />
240
+ <geom pos="0.0039635 0 -0.054" type="mesh" rgba="0.2 0.2 0.2 1" mesh="head_link" />
241
+ <geom pos="0.0039635 0 -0.054" quat="1 0 0 0" type="mesh" contype="0" conaffinity="0"
242
+ group="1" density="0" rgba="0.7 0.7 0.7 1" mesh="waist_support_link" />
243
+ <geom pos="0.0039635 0 -0.054" quat="1 0 0 0" type="mesh" rgba="0.7 0.7 0.7 1"
244
+ mesh="waist_support_link" />
245
+ <site name="imu" size="0.01" pos="-0.03959 -0.00224 0.13792" />
246
+ <body name="left_shoulder_pitch_link" pos="0.0039563 0.10022 0.23778"
247
+ quat="0.990264 0.139201 1.38722e-05 -9.86868e-05">
248
+ <inertial pos="0 0.035892 -0.011628" quat="0.654152 0.0130458 -0.326267 0.68225"
249
+ mass="0.718" diaginertia="0.000465864 0.000432842 0.000406394" />
250
+ <joint name="left_shoulder_pitch_joint" pos="0 0 0" axis="0 1 0"
251
+ range="-3.0892 2.6704" actuatorfrcrange="-25 25" class="arm_motor" />
252
+ <geom type="mesh" contype="0" conaffinity="0" group="1" density="0"
253
+ rgba="0.7 0.7 0.7 1" mesh="left_shoulder_pitch_link" />
254
+ <geom size="0.03 0.025" pos="0 0.04 -0.01" quat="0.707107 0 0.707107 0"
255
+ type="cylinder" rgba="0.7 0.7 0.7 1" />
256
+ <body name="left_shoulder_roll_link" pos="0 0.038 -0.013831"
257
+ quat="0.990268 -0.139172 0 0">
258
+ <inertial pos="-0.000227 0.00727 -0.063243"
259
+ quat="0.701256 -0.0196223 -0.00710317 0.712604" mass="0.643"
260
+ diaginertia="0.000691311 0.000618011 0.000388977" />
261
+ <joint name="left_shoulder_roll_joint" pos="0 0 0" axis="1 0 0"
262
+ range="-1.5882 2.2515" actuatorfrcrange="-25 25" class="arm_motor" />
263
+ <geom type="mesh" contype="0" conaffinity="0" group="1" density="0"
264
+ rgba="0.7 0.7 0.7 1" mesh="left_shoulder_roll_link" />
265
+ <geom size="0.03 0.015" pos="-0.004 0.006 -0.053" type="cylinder"
266
+ rgba="0.7 0.7 0.7 1" />
267
+ <body name="left_shoulder_yaw_link" pos="0 0.00624 -0.1032">
268
+ <inertial pos="0.010773 -0.002949 -0.072009"
269
+ quat="0.716879 -0.0964829 -0.0679942 0.687134" mass="0.734"
270
+ diaginertia="0.00106187 0.00103217 0.000400661" />
271
+ <joint name="left_shoulder_yaw_joint" pos="0 0 0" axis="0 0 1"
272
+ range="-2.618 2.618" actuatorfrcrange="-25 25" class="arm_motor" />
273
+ <geom type="mesh" contype="0" conaffinity="0" group="1" density="0"
274
+ rgba="0.7 0.7 0.7 1" mesh="left_shoulder_yaw_link" />
275
+ <geom type="mesh" rgba="0.7 0.7 0.7 1" mesh="left_shoulder_yaw_link" />
276
+ <body name="left_elbow_link" pos="0.015783 0 -0.080518">
277
+ <inertial pos="0.064956 0.004454 -0.010062"
278
+ quat="0.541765 0.636132 0.388821 0.388129" mass="0.6"
279
+ diaginertia="0.000443035 0.000421612 0.000259353" />
280
+ <joint name="left_elbow_joint" pos="0 0 0" axis="0 1 0" range="-1.0472 2.0944"
281
+ actuatorfrcrange="-25 25" class="arm_motor" />
282
+ <geom type="mesh" contype="0" conaffinity="0" group="1" density="0"
283
+ rgba="0.7 0.7 0.7 1" mesh="left_elbow_link" />
284
+ <geom type="mesh" rgba="0.7 0.7 0.7 1" mesh="left_elbow_link" />
285
+ <body name="left_wrist_roll_link" pos="0.1 0.00188791 -0.01">
286
+ <inertial pos="0.0171394 0.000537591 4.8864e-07"
287
+ quat="0.575338 0.411667 -0.574906 0.411094" mass="0.085445"
288
+ diaginertia="5.48211e-05 4.96646e-05 3.57798e-05" />
289
+ <joint name="left_wrist_roll_joint" pos="0 0 0" axis="1 0 0"
290
+ range="-1.97222 1.97222" actuatorfrcrange="-25 25" class="arm_motor" />
291
+ <geom type="mesh" contype="0" conaffinity="0" group="1" density="0"
292
+ rgba="0.7 0.7 0.7 1" mesh="left_wrist_roll_link" />
293
+ <geom type="mesh" rgba="0.7 0.7 0.7 1" mesh="left_wrist_roll_link" />
294
+ <body name="left_wrist_pitch_link" pos="0.038 0 0">
295
+ <inertial pos="0.0229999 -0.00111685 -0.00111658"
296
+ quat="0.249998 0.661363 0.293036 0.643608" mass="0.48405"
297
+ diaginertia="0.000430353 0.000429873 0.000164648" />
298
+ <joint name="left_wrist_pitch_joint" pos="0 0 0" axis="0 1 0"
299
+ range="-1.61443 1.61443" actuatorfrcrange="-5 5" class="wrist_motor" />
300
+ <geom type="mesh" contype="0" conaffinity="0" group="1" density="0"
301
+ rgba="0.7 0.7 0.7 1" mesh="left_wrist_pitch_link" />
302
+ <geom type="mesh" rgba="0.7 0.7 0.7 1" mesh="left_wrist_pitch_link" />
303
+ <body name="left_wrist_yaw_link" pos="0.046 0 0">
304
+ <inertial pos="0.0885506 0.00212216 -0.000374562"
305
+ quat="0.487149 0.493844 0.513241 0.505358" mass="0.457415"
306
+ diaginertia="0.00105989 0.000895419 0.000323842" />
307
+ <joint name="left_wrist_yaw_joint" pos="0 0 0" axis="0 0 1"
308
+ range="-1.61443 1.61443" actuatorfrcrange="-5 5" class="wrist_motor" />
309
+ <geom type="mesh" contype="0" conaffinity="0" group="1" density="0"
310
+ rgba="0.7 0.7 0.7 1" mesh="left_wrist_yaw_link" />
311
+ <geom type="mesh" rgba="0.7 0.7 0.7 1" mesh="left_wrist_yaw_link" />
312
+ <geom pos="0.0415 0.003 0" quat="1 0 0 0" type="mesh" contype="0"
313
+ conaffinity="0" group="1" density="0" rgba="0.7 0.7 0.7 1"
314
+ mesh="left_hand_palm_link" />
315
+ <geom pos="0.0415 0.003 0" quat="1 0 0 0" type="mesh" rgba="0.7 0.7 0.7 1"
316
+ mesh="left_hand_palm_link" />
317
+ <body name="left_hand_thumb_0_link" pos="0.067 0.003 0">
318
+ <inertial pos="-0.000884246 -0.00863407 0.000944293"
319
+ quat="0.462991 0.643965 -0.460173 0.398986" mass="0.0862366"
320
+ diaginertia="1.6546e-05 1.60058e-05 1.43741e-05" />
321
+ <joint name="left_hand_thumb_0_joint" pos="0 0 0" axis="0 1 0"
322
+ range="-1.0472 1.0472" actuatorfrcrange="-2.45 2.45"
323
+ class="finger_motor" />
324
+ <geom type="mesh" contype="0" conaffinity="0" group="1" density="0"
325
+ rgba="0.7 0.7 0.7 1" mesh="left_hand_thumb_0_link" />
326
+ <geom type="mesh" rgba="0.7 0.7 0.7 1" mesh="left_hand_thumb_0_link" />
327
+ <body name="left_hand_thumb_1_link" pos="-0.0025 -0.0193 0">
328
+ <inertial pos="-0.000827888 -0.0354744 -0.0003809"
329
+ quat="0.685598 0.705471 -0.15207 0.0956069" mass="0.0588507"
330
+ diaginertia="1.28514e-05 1.22902e-05 5.9666e-06" />
331
+ <joint name="left_hand_thumb_1_joint" pos="0 0 0" axis="0 0 1"
332
+ range="-0.724312 1.0472" actuatorfrcrange="-1.4 1.4"
333
+ class="finger_motor" />
334
+ <geom type="mesh" contype="0" conaffinity="0" group="1" density="0"
335
+ rgba="0.7 0.7 0.7 1" mesh="left_hand_thumb_1_link" />
336
+ <geom size="0.01 0.015 0.01" pos="-0.001 -0.032 0" type="box"
337
+ rgba="0.7 0.7 0.7 1" />
338
+ <body name="left_hand_thumb_2_link" pos="0 -0.0458 0">
339
+ <inertial pos="-0.00171735 -0.0262819 0.000107789"
340
+ quat="0.703174 0.710977 -0.00017564 -0.00766553" mass="0.0203063"
341
+ diaginertia="4.61314e-06 3.86645e-06 1.53495e-06" />
342
+ <joint name="left_hand_thumb_2_joint" pos="0 0 0" axis="0 0 1"
343
+ range="0 1.74533" actuatorfrcrange="-1.4 1.4" class="finger_motor" />
344
+ <geom type="mesh" contype="0" conaffinity="0" group="1" density="0"
345
+ rgba="0.7 0.7 0.7 1" mesh="left_hand_thumb_2_link" />
346
+ <geom type="mesh" rgba="0.7 0.7 0.7 1" mesh="left_hand_thumb_2_link" />
347
+ </body>
348
+ </body>
349
+ </body>
350
+ <body name="left_hand_middle_0_link" pos="0.1192 0.0046 -0.0285">
351
+ <inertial pos="0.0354744 0.000827888 0.0003809"
352
+ quat="0.391313 0.552395 0.417187 0.606373" mass="0.0588507"
353
+ diaginertia="1.28514e-05 1.22902e-05 5.9666e-06" />
354
+ <joint name="left_hand_middle_0_joint" pos="0 0 0" axis="0 0 1"
355
+ range="-1.5708 0" actuatorfrcrange="-1.4 1.4" class="finger_motor" />
356
+ <geom type="mesh" contype="0" conaffinity="0" group="1" density="0"
357
+ rgba="0.7 0.7 0.7 1" mesh="left_hand_middle_0_link" />
358
+ <geom type="mesh" rgba="0.7 0.7 0.7 1" mesh="left_hand_middle_0_link" />
359
+ <body name="left_hand_middle_1_link" pos="0.0458 0 0">
360
+ <inertial pos="0.0262819 0.00171735 -0.000107789"
361
+ quat="0.502612 0.491799 0.502639 0.502861" mass="0.0203063"
362
+ diaginertia="4.61314e-06 3.86645e-06 1.53495e-06" />
363
+ <joint name="left_hand_middle_1_joint" pos="0 0 0" axis="0 0 1"
364
+ range="-1.74533 0" actuatorfrcrange="-1.4 1.4" class="finger_motor" />
365
+ <geom type="mesh" contype="0" conaffinity="0" group="1" density="0"
366
+ rgba="0.7 0.7 0.7 1" mesh="left_hand_middle_1_link" />
367
+ <geom type="mesh" rgba="0.7 0.7 0.7 1" mesh="left_hand_middle_1_link" />
368
+ </body>
369
+ </body>
370
+ <body name="left_hand_index_0_link" pos="0.1192 0.0046 0.0285">
371
+ <inertial pos="0.0354744 0.000827888 0.0003809"
372
+ quat="0.391313 0.552395 0.417187 0.606373" mass="0.0588507"
373
+ diaginertia="1.28514e-05 1.22902e-05 5.9666e-06" />
374
+ <joint name="left_hand_index_0_joint" pos="0 0 0" axis="0 0 1"
375
+ range="-1.5708 0" actuatorfrcrange="-1.4 1.4" class="finger_motor" />
376
+ <geom type="mesh" contype="0" conaffinity="0" group="1" density="0"
377
+ rgba="0.7 0.7 0.7 1" mesh="left_hand_index_0_link" />
378
+ <geom type="mesh" rgba="0.7 0.7 0.7 1" mesh="left_hand_index_0_link" />
379
+ <body name="left_hand_index_1_link" pos="0.0458 0 0">
380
+ <inertial pos="0.0262819 0.00171735 -0.000107789"
381
+ quat="0.502612 0.491799 0.502639 0.502861" mass="0.0203063"
382
+ diaginertia="4.61314e-06 3.86645e-06 1.53495e-06" />
383
+ <joint name="left_hand_index_1_joint" pos="0 0 0" axis="0 0 1"
384
+ range="-1.74533 0" actuatorfrcrange="-1.4 1.4" class="finger_motor" />
385
+ <geom type="mesh" contype="0" conaffinity="0" group="1" density="0"
386
+ rgba="0.7 0.7 0.7 1" mesh="left_hand_index_1_link" />
387
+ <geom type="mesh" rgba="0.7 0.7 0.7 1" mesh="left_hand_index_1_link" />
388
+ </body>
389
+ </body>
390
+ </body>
391
+ </body>
392
+ </body>
393
+ </body>
394
+ </body>
395
+ </body>
396
+ </body>
397
+ <body name="right_shoulder_pitch_link" pos="0.0039563 -0.10021 0.23778"
398
+ quat="0.990264 -0.139201 1.38722e-05 9.86868e-05">
399
+ <inertial pos="0 -0.035892 -0.011628" quat="0.68225 -0.326267 0.0130458 0.654152"
400
+ mass="0.718" diaginertia="0.000465864 0.000432842 0.000406394" />
401
+ <joint name="right_shoulder_pitch_joint" pos="0 0 0" axis="0 1 0"
402
+ range="-3.0892 2.6704" actuatorfrcrange="-25 25" class="arm_motor" />
403
+ <geom type="mesh" contype="0" conaffinity="0" group="1" density="0"
404
+ rgba="0.7 0.7 0.7 1" mesh="right_shoulder_pitch_link" />
405
+ <geom size="0.03 0.025" pos="0 -0.04 -0.01" quat="0.707107 0 0.707107 0"
406
+ type="cylinder" rgba="0.7 0.7 0.7 1" />
407
+ <body name="right_shoulder_roll_link" pos="0 -0.038 -0.013831"
408
+ quat="0.990268 0.139172 0 0">
409
+ <inertial pos="-0.000227 -0.00727 -0.063243"
410
+ quat="0.712604 -0.00710317 -0.0196223 0.701256" mass="0.643"
411
+ diaginertia="0.000691311 0.000618011 0.000388977" />
412
+ <joint name="right_shoulder_roll_joint" pos="0 0 0" axis="1 0 0"
413
+ range="-2.2515 1.5882" actuatorfrcrange="-25 25" class="arm_motor" />
414
+ <geom type="mesh" contype="0" conaffinity="0" group="1" density="0"
415
+ rgba="0.7 0.7 0.7 1" mesh="right_shoulder_roll_link" />
416
+ <geom size="0.03 0.015" pos="-0.004 -0.006 -0.053" type="cylinder"
417
+ rgba="0.7 0.7 0.7 1" />
418
+ <body name="right_shoulder_yaw_link" pos="0 -0.00624 -0.1032">
419
+ <inertial pos="0.010773 0.002949 -0.072009"
420
+ quat="0.687134 -0.0679942 -0.0964829 0.716879" mass="0.734"
421
+ diaginertia="0.00106187 0.00103217 0.000400661" />
422
+ <joint name="right_shoulder_yaw_joint" pos="0 0 0" axis="0 0 1"
423
+ range="-2.618 2.618" actuatorfrcrange="-25 25" class="arm_motor" />
424
+ <geom type="mesh" contype="0" conaffinity="0" group="1" density="0"
425
+ rgba="0.7 0.7 0.7 1" mesh="right_shoulder_yaw_link" />
426
+ <geom type="mesh" rgba="0.7 0.7 0.7 1" mesh="right_shoulder_yaw_link" />
427
+ <body name="right_elbow_link" pos="0.015783 0 -0.080518">
428
+ <inertial pos="0.064956 -0.004454 -0.010062"
429
+ quat="0.388129 0.388821 0.636132 0.541765" mass="0.6"
430
+ diaginertia="0.000443035 0.000421612 0.000259353" />
431
+ <joint name="right_elbow_joint" pos="0 0 0" axis="0 1 0" range="-1.0472 2.0944"
432
+ actuatorfrcrange="-25 25" class="arm_motor" />
433
+ <geom type="mesh" contype="0" conaffinity="0" group="1" density="0"
434
+ rgba="0.7 0.7 0.7 1" mesh="right_elbow_link" />
435
+ <geom type="mesh" rgba="0.7 0.7 0.7 1" mesh="right_elbow_link" />
436
+ <body name="right_wrist_roll_link" pos="0.1 -0.00188791 -0.01">
437
+ <inertial pos="0.0171394 -0.000537591 4.8864e-07"
438
+ quat="0.411667 0.575338 -0.411094 0.574906" mass="0.085445"
439
+ diaginertia="5.48211e-05 4.96646e-05 3.57798e-05" />
440
+ <joint name="right_wrist_roll_joint" pos="0 0 0" axis="1 0 0"
441
+ range="-1.97222 1.97222" actuatorfrcrange="-25 25" class="arm_motor" />
442
+ <geom type="mesh" contype="0" conaffinity="0" group="1" density="0"
443
+ rgba="0.7 0.7 0.7 1" mesh="right_wrist_roll_link" />
444
+ <geom type="mesh" rgba="0.7 0.7 0.7 1" mesh="right_wrist_roll_link" />
445
+ <body name="right_wrist_pitch_link" pos="0.038 0 0">
446
+ <inertial pos="0.0229999 0.00111685 -0.00111658"
447
+ quat="0.643608 0.293036 0.661363 0.249998" mass="0.48405"
448
+ diaginertia="0.000430353 0.000429873 0.000164648" />
449
+ <joint name="right_wrist_pitch_joint" pos="0 0 0" axis="0 1 0"
450
+ range="-1.61443 1.61443" actuatorfrcrange="-5 5" class="wrist_motor" />
451
+ <geom type="mesh" contype="0" conaffinity="0" group="1" density="0"
452
+ rgba="0.7 0.7 0.7 1" mesh="right_wrist_pitch_link" />
453
+ <geom type="mesh" rgba="0.7 0.7 0.7 1" mesh="right_wrist_pitch_link" />
454
+ <body name="right_wrist_yaw_link" pos="0.046 0 0">
455
+ <inertial pos="0.0885506 -0.00212216 0.000573742"
456
+ quat="0.507224 0.511377 0.494292 0.486717" mass="0.457415"
457
+ diaginertia="0.0010598 0.000895373 0.0003238" />
458
+ <joint name="right_wrist_yaw_joint" pos="0 0 0" axis="0 0 1"
459
+ range="-1.61443 1.61443" actuatorfrcrange="-5 5" class="wrist_motor" />
460
+ <geom type="mesh" contype="0" conaffinity="0" group="1" density="0"
461
+ rgba="0.7 0.7 0.7 1" mesh="right_wrist_yaw_link" />
462
+ <geom type="mesh" rgba="0.7 0.7 0.7 1" mesh="right_wrist_yaw_link" />
463
+ <geom pos="0.0415 -0.003 0" quat="1 0 0 0" type="mesh" contype="0"
464
+ conaffinity="0" group="1" density="0" rgba="0.7 0.7 0.7 1"
465
+ mesh="right_hand_palm_link" />
466
+ <geom pos="0.0415 -0.003 0" quat="1 0 0 0" type="mesh"
467
+ rgba="0.7 0.7 0.7 1" mesh="right_hand_palm_link" />
468
+ <body name="right_hand_thumb_0_link" pos="0.067 -0.003 0">
469
+ <inertial pos="-0.000884246 0.00863407 0.000944293"
470
+ quat="0.643965 0.462991 -0.398986 0.460173" mass="0.0862366"
471
+ diaginertia="1.6546e-05 1.60058e-05 1.43741e-05" />
472
+ <joint name="right_hand_thumb_0_joint" pos="0 0 0" axis="0 1 0"
473
+ range="-1.0472 1.0472" actuatorfrcrange="-2.45 2.45"
474
+ class="finger_motor" />
475
+ <geom type="mesh" contype="0" conaffinity="0" group="1" density="0"
476
+ rgba="0.7 0.7 0.7 1" mesh="right_hand_thumb_0_link" />
477
+ <geom type="mesh" rgba="0.7 0.7 0.7 1" mesh="right_hand_thumb_0_link" />
478
+ <body name="right_hand_thumb_1_link" pos="-0.0025 0.0193 0">
479
+ <inertial pos="-0.000827888 0.0354744 -0.0003809"
480
+ quat="0.705471 0.685598 -0.0956069 0.15207" mass="0.0588507"
481
+ diaginertia="1.28514e-05 1.22902e-05 5.9666e-06" />
482
+ <joint name="right_hand_thumb_1_joint" pos="0 0 0" axis="0 0 1"
483
+ range="-1.0472 0.724312" actuatorfrcrange="-1.4 1.4"
484
+ class="finger_motor" />
485
+ <geom type="mesh" contype="0" conaffinity="0" group="1" density="0"
486
+ rgba="0.7 0.7 0.7 1" mesh="right_hand_thumb_1_link" />
487
+ <geom size="0.01 0.015 0.01" pos="-0.001 0.032 0" type="box"
488
+ rgba="0.7 0.7 0.7 1" />
489
+ <body name="right_hand_thumb_2_link" pos="0 0.0458 0">
490
+ <inertial pos="-0.00171735 0.0262819 0.000107789"
491
+ quat="0.710977 0.703174 0.00766553 0.00017564" mass="0.0203063"
492
+ diaginertia="4.61314e-06 3.86645e-06 1.53495e-06" />
493
+ <joint name="right_hand_thumb_2_joint" pos="0 0 0" axis="0 0 1"
494
+ range="-1.74533 0" actuatorfrcrange="-1.4 1.4"
495
+ class="finger_motor" />
496
+ <geom type="mesh" contype="0" conaffinity="0" group="1" density="0"
497
+ rgba="0.7 0.7 0.7 1" mesh="right_hand_thumb_2_link" />
498
+ <geom type="mesh" rgba="0.7 0.7 0.7 1"
499
+ mesh="right_hand_thumb_2_link" />
500
+ </body>
501
+ </body>
502
+ </body>
503
+ <body name="right_hand_middle_0_link" pos="0.1192 -0.0046 -0.0285">
504
+ <inertial pos="0.0354744 -0.000827888 0.0003809"
505
+ quat="0.606373 0.417187 0.552395 0.391313" mass="0.0588507"
506
+ diaginertia="1.28514e-05 1.22902e-05 5.9666e-06" />
507
+ <joint name="right_hand_middle_0_joint" pos="0 0 0" axis="0 0 1"
508
+ range="0 1.5708" actuatorfrcrange="-1.4 1.4" class="finger_motor" />
509
+ <geom type="mesh" contype="0" conaffinity="0" group="1" density="0"
510
+ rgba="0.7 0.7 0.7 1" mesh="right_hand_middle_0_link" />
511
+ <geom type="mesh" rgba="0.7 0.7 0.7 1" mesh="right_hand_middle_0_link" />
512
+ <body name="right_hand_middle_1_link" pos="0.0458 0 0">
513
+ <inertial pos="0.0262819 -0.00171735 -0.000107789"
514
+ quat="0.502861 0.502639 0.491799 0.502612" mass="0.0203063"
515
+ diaginertia="4.61314e-06 3.86645e-06 1.53495e-06" />
516
+ <joint name="right_hand_middle_1_joint" pos="0 0 0" axis="0 0 1"
517
+ range="0 1.74533" actuatorfrcrange="-1.4 1.4" class="finger_motor" />
518
+ <geom type="mesh" contype="0" conaffinity="0" group="1" density="0"
519
+ rgba="0.7 0.7 0.7 1" mesh="right_hand_middle_1_link" />
520
+ <geom type="mesh" rgba="0.7 0.7 0.7 1" mesh="right_hand_middle_1_link" />
521
+ </body>
522
+ </body>
523
+ <body name="right_hand_index_0_link" pos="0.1192 -0.0046 0.0285">
524
+ <inertial pos="0.0354744 -0.000827888 0.0003809"
525
+ quat="0.606373 0.417187 0.552395 0.391313" mass="0.0588507"
526
+ diaginertia="1.28514e-05 1.22902e-05 5.9666e-06" />
527
+ <joint name="right_hand_index_0_joint" pos="0 0 0" axis="0 0 1"
528
+ range="0 1.5708" actuatorfrcrange="-1.4 1.4" class="finger_motor" />
529
+ <geom type="mesh" contype="0" conaffinity="0" group="1" density="0"
530
+ rgba="0.7 0.7 0.7 1" mesh="right_hand_index_0_link" />
531
+ <geom type="mesh" rgba="0.7 0.7 0.7 1" mesh="right_hand_index_0_link" />
532
+ <body name="right_hand_index_1_link" pos="0.0458 0 0">
533
+ <inertial pos="0.0262819 -0.00171735 -0.000107789"
534
+ quat="0.502861 0.502639 0.491799 0.502612" mass="0.0203063"
535
+ diaginertia="4.61314e-06 3.86645e-06 1.53495e-06" />
536
+ <joint name="right_hand_index_1_joint" pos="0 0 0" axis="0 0 1"
537
+ range="0 1.74533" actuatorfrcrange="-1.4 1.4" class="finger_motor" />
538
+ <geom type="mesh" contype="0" conaffinity="0" group="1" density="0"
539
+ rgba="0.7 0.7 0.7 1" mesh="right_hand_index_1_link" />
540
+ <geom type="mesh" rgba="0.7 0.7 0.7 1" mesh="right_hand_index_1_link" />
541
+ </body>
542
+ </body>
543
+ </body>
544
+ </body>
545
+ </body>
546
+ </body>
547
+ </body>
548
+ </body>
549
+ </body>
550
+ </body>
551
+ </body>
552
+ </body>
553
+ </body>
554
+ </worldbody>
555
+
556
+ <actuator>
557
+ <motor name="left_hip_pitch" joint="left_hip_pitch_joint" ctrlrange="-88 88" />
558
+ <motor name="left_hip_roll" joint="left_hip_roll_joint" ctrlrange="-88 88" />
559
+ <motor name="left_hip_yaw" joint="left_hip_yaw_joint" ctrlrange="-88 88" />
560
+ <motor name="left_knee" joint="left_knee_joint" ctrlrange="-139 139" />
561
+ <motor name="left_ankle_pitch" joint="left_ankle_pitch_joint" ctrlrange="-50 50" />
562
+ <motor name="left_ankle_roll" joint="left_ankle_roll_joint" ctrlrange="-50 50" />
563
+
564
+ <motor name="right_hip_pitch" joint="right_hip_pitch_joint" ctrlrange="-88 88" />
565
+ <motor name="right_hip_roll" joint="right_hip_roll_joint" ctrlrange="-88 88" />
566
+ <motor name="right_hip_yaw" joint="right_hip_yaw_joint" ctrlrange="-88 88" />
567
+ <motor name="right_knee" joint="right_knee_joint" ctrlrange="-139 139" />
568
+ <motor name="right_ankle_pitch" joint="right_ankle_pitch_joint" ctrlrange="-50 50" />
569
+ <motor name="right_ankle_roll" joint="right_ankle_roll_joint" ctrlrange="-50 50" />
570
+
571
+ <motor name="waist_yaw" joint="waist_yaw_joint" ctrlrange="-88 88" />
572
+ <motor name="waist_roll" joint="waist_roll_joint" ctrlrange="-50 50" />
573
+ <motor name="waist_pitch" joint="waist_pitch_joint" ctrlrange="-50 50" />
574
+
575
+ <motor name="left_shoulder_pitch" joint="left_shoulder_pitch_joint" ctrlrange="-25 25" />
576
+ <motor name="left_shoulder_roll" joint="left_shoulder_roll_joint" ctrlrange="-25 25" />
577
+ <motor name="left_shoulder_yaw" joint="left_shoulder_yaw_joint" ctrlrange="-25 25" />
578
+ <motor name="left_elbow" joint="left_elbow_joint" ctrlrange="-25 25" />
579
+ <motor name="left_wrist_roll" joint="left_wrist_roll_joint" ctrlrange="-25 25" />
580
+ <motor name="left_wrist_pitch" joint="left_wrist_pitch_joint" ctrlrange="-5 5" />
581
+ <motor name="left_wrist_yaw" joint="left_wrist_yaw_joint" ctrlrange="-5 5" />
582
+
583
+ <motor name="left_hand_thumb_0" joint="left_hand_thumb_0_joint" ctrlrange="-2.45 2.45" />
584
+ <motor name="left_hand_thumb_1" joint="left_hand_thumb_1_joint" ctrlrange="-1.4 1.4" />
585
+ <motor name="left_hand_thumb_2" joint="left_hand_thumb_2_joint" ctrlrange="-1.4 1.4" />
586
+ <motor name="left_hand_middle_0" joint="left_hand_middle_0_joint" ctrlrange="-1.4 1.4" />
587
+ <motor name="left_hand_middle_1" joint="left_hand_middle_1_joint" ctrlrange="-1.4 1.4" />
588
+ <motor name="left_hand_index_0" joint="left_hand_index_0_joint" ctrlrange="-1.4 1.4" />
589
+ <motor name="left_hand_index_1" joint="left_hand_index_1_joint" ctrlrange="-1.4 1.4" />
590
+
591
+ <motor name="right_shoulder_pitch" joint="right_shoulder_pitch_joint" ctrlrange="-25 25" />
592
+ <motor name="right_shoulder_roll" joint="right_shoulder_roll_joint" ctrlrange="-25 25" />
593
+ <motor name="right_shoulder_yaw" joint="right_shoulder_yaw_joint" ctrlrange="-25 25" />
594
+ <motor name="right_elbow" joint="right_elbow_joint" ctrlrange="-25 25" />
595
+ <motor name="right_wrist_roll" joint="right_wrist_roll_joint" ctrlrange="-25 25" />
596
+ <motor name="right_wrist_pitch" joint="right_wrist_pitch_joint" ctrlrange="-5 5" />
597
+ <motor name="right_wrist_yaw" joint="right_wrist_yaw_joint" ctrlrange="-5 5" />
598
+
599
+ <motor name="right_hand_thumb_0" joint="right_hand_thumb_0_joint" ctrlrange="-2.45 2.45" />
600
+ <motor name="right_hand_thumb_1" joint="right_hand_thumb_1_joint" ctrlrange="-1.4 1.4" />
601
+ <motor name="right_hand_thumb_2" joint="right_hand_thumb_2_joint" ctrlrange="-1.4 1.4" />
602
+ <motor name="right_hand_middle_0" joint="right_hand_middle_0_joint" ctrlrange="-1.4 1.4" />
603
+ <motor name="right_hand_middle_1" joint="right_hand_middle_1_joint" ctrlrange="-1.4 1.4" />
604
+ <motor name="right_hand_index_0" joint="right_hand_index_0_joint" ctrlrange="-1.4 1.4" />
605
+ <motor name="right_hand_index_1" joint="right_hand_index_1_joint" ctrlrange="-1.4 1.4" />
606
+ </actuator>
607
+
608
+ <sensor>
609
+ <jointpos name="left_hip_pitch_pos" joint="left_hip_pitch_joint" />
610
+ <jointpos name="left_hip_roll_pos" joint="left_hip_roll_joint" />
611
+ <jointpos name="left_hip_yaw_pos" joint="left_hip_yaw_joint" />
612
+ <jointpos name="left_knee_pos" joint="left_knee_joint" />
613
+ <jointpos name="left_ankle_pitch_pos" joint="left_ankle_pitch_joint" />
614
+ <jointpos name="left_ankle_roll_pos" joint="left_ankle_roll_joint" />
615
+ <jointpos name="right_hip_pitch_pos" joint="right_hip_pitch_joint" />
616
+ <jointpos name="right_hip_roll_pos" joint="right_hip_roll_joint" />
617
+ <jointpos name="right_hip_yaw_pos" joint="right_hip_yaw_joint" />
618
+ <jointpos name="right_knee_pos" joint="right_knee_joint" />
619
+ <jointpos name="right_ankle_pitch_pos" joint="right_ankle_pitch_joint" />
620
+ <jointpos name="right_ankle_roll_pos" joint="right_ankle_roll_joint" />
621
+ <jointpos name="waist_yaw_pos" joint="waist_yaw_joint" />
622
+ <jointpos name="waist_roll_pos" joint="waist_roll_joint" />
623
+ <jointpos name="waist_pitch_pos" joint="waist_pitch_joint" />
624
+ <jointpos name="left_shoulder_pitch_pos" joint="left_shoulder_pitch_joint" />
625
+ <jointpos name="left_shoulder_roll_pos" joint="left_shoulder_roll_joint" />
626
+ <jointpos name="left_shoulder_yaw_pos" joint="left_shoulder_yaw_joint" />
627
+ <jointpos name="left_elbow_pos" joint="left_elbow_joint" />
628
+ <jointpos name="left_wrist_roll_pos" joint="left_wrist_roll_joint" />
629
+ <jointpos name="left_wrist_pitch_pos" joint="left_wrist_pitch_joint" />
630
+ <jointpos name="left_wrist_yaw_pos" joint="left_wrist_yaw_joint" />
631
+ <jointpos name="right_shoulder_pitch_pos" joint="right_shoulder_pitch_joint" />
632
+ <jointpos name="right_shoulder_roll_pos" joint="right_shoulder_roll_joint" />
633
+ <jointpos name="right_shoulder_yaw_pos" joint="right_shoulder_yaw_joint" />
634
+ <jointpos name="right_elbow_pos" joint="right_elbow_joint" />
635
+ <jointpos name="right_wrist_roll_pos" joint="right_wrist_roll_joint" />
636
+ <jointpos name="right_wrist_pitch_pos" joint="right_wrist_pitch_joint" />
637
+ <jointpos name="right_wrist_yaw_pos" joint="right_wrist_yaw_joint" />
638
+
639
+ <jointpos name="left_hand_thumb_0_pos" joint="left_hand_thumb_0_joint" />
640
+ <jointpos name="left_hand_thumb_1_pos" joint="left_hand_thumb_1_joint" />
641
+ <jointpos name="left_hand_thumb_2_pos" joint="left_hand_thumb_2_joint" />
642
+ <jointpos name="left_hand_middle_0_pos" joint="left_hand_middle_0_joint" />
643
+ <jointpos name="left_hand_middle_1_pos" joint="left_hand_middle_1_joint" />
644
+ <jointpos name="left_hand_index_0_pos" joint="left_hand_index_0_joint" />
645
+ <jointpos name="left_hand_index_1_pos" joint="left_hand_index_1_joint" />
646
+ <jointpos name="right_hand_thumb_0_pos" joint="right_hand_thumb_0_joint" />
647
+ <jointpos name="right_hand_thumb_1_pos" joint="right_hand_thumb_1_joint" />
648
+ <jointpos name="right_hand_thumb_2_pos" joint="right_hand_thumb_2_joint" />
649
+ <jointpos name="right_hand_middle_0_pos" joint="right_hand_middle_0_joint" />
650
+ <jointpos name="right_hand_middle_1_pos" joint="right_hand_middle_1_joint" />
651
+ <jointpos name="right_hand_index_0_pos" joint="right_hand_index_0_joint" />
652
+ <jointpos name="right_hand_index_1_pos" joint="right_hand_index_1_joint" />
653
+
654
+ <jointvel name="left_hip_pitch_vel" joint="left_hip_pitch_joint" />
655
+ <jointvel name="left_hip_roll_vel" joint="left_hip_roll_joint" />
656
+ <jointvel name="left_hip_yaw_vel" joint="left_hip_yaw_joint" />
657
+ <jointvel name="left_knee_vel" joint="left_knee_joint" />
658
+ <jointvel name="left_ankle_pitch_vel" joint="left_ankle_pitch_joint" />
659
+ <jointvel name="left_ankle_roll_vel" joint="left_ankle_roll_joint" />
660
+ <jointvel name="right_hip_pitch_vel" joint="right_hip_pitch_joint" />
661
+ <jointvel name="right_hip_roll_vel" joint="right_hip_roll_joint" />
662
+ <jointvel name="right_hip_yaw_vel" joint="right_hip_yaw_joint" />
663
+ <jointvel name="right_knee_vel" joint="right_knee_joint" />
664
+ <jointvel name="right_ankle_pitch_vel" joint="right_ankle_pitch_joint" />
665
+ <jointvel name="right_ankle_roll_vel" joint="right_ankle_roll_joint" />
666
+ <jointvel name="waist_yaw_vel" joint="waist_yaw_joint" />
667
+ <jointvel name="waist_roll_vel" joint="waist_roll_joint" />
668
+ <jointvel name="waist_pitch_vel" joint="waist_pitch_joint" />
669
+ <jointvel name="left_shoulder_pitch_vel" joint="left_shoulder_pitch_joint" />
670
+ <jointvel name="left_shoulder_roll_vel" joint="left_shoulder_roll_joint" />
671
+ <jointvel name="left_shoulder_yaw_vel" joint="left_shoulder_yaw_joint" />
672
+ <jointvel name="left_elbow_vel" joint="left_elbow_joint" />
673
+ <jointvel name="left_wrist_roll_vel" joint="left_wrist_roll_joint" />
674
+ <jointvel name="left_wrist_pitch_vel" joint="left_wrist_pitch_joint" />
675
+ <jointvel name="left_wrist_yaw_vel" joint="left_wrist_yaw_joint" />
676
+ <jointvel name="right_shoulder_pitch_vel" joint="right_shoulder_pitch_joint" />
677
+ <jointvel name="right_shoulder_roll_vel" joint="right_shoulder_roll_joint" />
678
+ <jointvel name="right_shoulder_yaw_vel" joint="right_shoulder_yaw_joint" />
679
+ <jointvel name="right_elbow_vel" joint="right_elbow_joint" />
680
+ <jointvel name="right_wrist_roll_vel" joint="right_wrist_roll_joint" />
681
+ <jointvel name="right_wrist_pitch_vel" joint="right_wrist_pitch_joint" />
682
+ <jointvel name="right_wrist_yaw_vel" joint="right_wrist_yaw_joint" />
683
+
684
+ <jointvel name="left_hand_thumb_0_vel" joint="left_hand_thumb_0_joint" />
685
+ <jointvel name="left_hand_thumb_1_vel" joint="left_hand_thumb_1_joint" />
686
+ <jointvel name="left_hand_thumb_2_vel" joint="left_hand_thumb_2_joint" />
687
+ <jointvel name="left_hand_middle_0_vel" joint="left_hand_middle_0_joint" />
688
+ <jointvel name="left_hand_middle_1_vel" joint="left_hand_middle_1_joint" />
689
+ <jointvel name="left_hand_index_0_vel" joint="left_hand_index_0_joint" />
690
+ <jointvel name="left_hand_index_1_vel" joint="left_hand_index_1_joint" />
691
+ <jointvel name="right_hand_thumb_0_vel" joint="right_hand_thumb_0_joint" />
692
+ <jointvel name="right_hand_thumb_1_vel" joint="right_hand_thumb_1_joint" />
693
+ <jointvel name="right_hand_thumb_2_vel" joint="right_hand_thumb_2_joint" />
694
+ <jointvel name="right_hand_middle_0_vel" joint="right_hand_middle_0_joint" />
695
+ <jointvel name="right_hand_middle_1_vel" joint="right_hand_middle_1_joint" />
696
+ <jointvel name="right_hand_index_0_vel" joint="right_hand_index_0_joint" />
697
+ <jointvel name="right_hand_index_1_vel" joint="right_hand_index_1_joint" />
698
+
699
+ <jointactuatorfrc name="left_hip_pitch_torque" joint="left_hip_pitch_joint" />
700
+ <jointactuatorfrc name="left_hip_roll_torque" joint="left_hip_roll_joint" />
701
+ <jointactuatorfrc name="left_hip_yaw_torque" joint="left_hip_yaw_joint" />
702
+ <jointactuatorfrc name="left_knee_torque" joint="left_knee_joint" />
703
+ <jointactuatorfrc name="left_ankle_pitch_torque" joint="left_ankle_pitch_joint" />
704
+ <jointactuatorfrc name="left_ankle_roll_torque" joint="left_ankle_roll_joint" />
705
+ <jointactuatorfrc name="right_hip_pitch_torque" joint="right_hip_pitch_joint" />
706
+ <jointactuatorfrc name="right_hip_roll_torque" joint="right_hip_roll_joint" />
707
+ <jointactuatorfrc name="right_hip_yaw_torque" joint="right_hip_yaw_joint" />
708
+ <jointactuatorfrc name="right_knee_torque" joint="right_knee_joint" />
709
+ <jointactuatorfrc name="right_ankle_pitch_torque" joint="right_ankle_pitch_joint" />
710
+ <jointactuatorfrc name="right_ankle_roll_torque" joint="right_ankle_roll_joint" />
711
+ <jointactuatorfrc name="waist_yaw_torque" joint="waist_yaw_joint" />
712
+ <jointactuatorfrc name="waist_roll_torque" joint="waist_roll_joint" />
713
+ <jointactuatorfrc name="waist_pitch_torque" joint="waist_pitch_joint" />
714
+ <jointactuatorfrc name="left_shoulder_pitch_torque" joint="left_shoulder_pitch_joint" />
715
+ <jointactuatorfrc name="left_shoulder_roll_torque" joint="left_shoulder_roll_joint" />
716
+ <jointactuatorfrc name="left_shoulder_yaw_torque" joint="left_shoulder_yaw_joint" />
717
+ <jointactuatorfrc name="left_elbow_torque" joint="left_elbow_joint" />
718
+ <jointactuatorfrc name="left_wrist_roll_torque" joint="left_wrist_roll_joint" />
719
+ <jointactuatorfrc name="left_wrist_pitch_torque" joint="left_wrist_pitch_joint" />
720
+ <jointactuatorfrc name="left_wrist_yaw_torque" joint="left_wrist_yaw_joint" />
721
+ <jointactuatorfrc name="right_shoulder_pitch_torque" joint="right_shoulder_pitch_joint" />
722
+ <jointactuatorfrc name="right_shoulder_roll_torque" joint="right_shoulder_roll_joint" />
723
+ <jointactuatorfrc name="right_shoulder_yaw_torque" joint="right_shoulder_yaw_joint" />
724
+ <jointactuatorfrc name="right_elbow_torque" joint="right_elbow_joint" />
725
+ <jointactuatorfrc name="right_wrist_roll_torque" joint="right_wrist_roll_joint" />
726
+ <jointactuatorfrc name="right_wrist_pitch_torque" joint="right_wrist_pitch_joint" />
727
+ <jointactuatorfrc name="right_wrist_yaw_torque" joint="right_wrist_yaw_joint" />
728
+
729
+ <jointactuatorfrc name="left_hand_thumb_0_torque" joint="left_hand_thumb_0_joint" />
730
+ <jointactuatorfrc name="left_hand_thumb_1_torque" joint="left_hand_thumb_1_joint" />
731
+ <jointactuatorfrc name="left_hand_thumb_2_torque" joint="left_hand_thumb_2_joint" />
732
+ <jointactuatorfrc name="left_hand_middle_0_torque" joint="left_hand_middle_0_joint" />
733
+ <jointactuatorfrc name="left_hand_middle_1_torque" joint="left_hand_middle_1_joint" />
734
+ <jointactuatorfrc name="left_hand_index_0_torque" joint="left_hand_index_0_joint" />
735
+ <jointactuatorfrc name="left_hand_index_1_torque" joint="left_hand_index_1_joint" />
736
+ <jointactuatorfrc name="right_hand_thumb_0_torque" joint="right_hand_thumb_0_joint" />
737
+ <jointactuatorfrc name="right_hand_thumb_1_torque" joint="right_hand_thumb_1_joint" />
738
+ <jointactuatorfrc name="right_hand_thumb_2_torque" joint="right_hand_thumb_2_joint" />
739
+ <jointactuatorfrc name="right_hand_middle_0_torque" joint="right_hand_middle_0_joint" />
740
+ <jointactuatorfrc name="right_hand_middle_1_torque" joint="right_hand_middle_1_joint" />
741
+ <jointactuatorfrc name="right_hand_index_0_torque" joint="right_hand_index_0_joint" />
742
+ <jointactuatorfrc name="right_hand_index_1_torque" joint="right_hand_index_1_joint" />
743
+
744
+ <framequat name="imu_quat" objtype="site" objname="imu" />
745
+ <gyro name="imu_gyro" site="imu" />
746
+ <accelerometer name="imu_acc" site="imu" />
747
+
748
+ <framepos name="frame_pos" objtype="site" objname="imu" />
749
+ <framelinvel name="frame_vel" objtype="site" objname="imu" />
750
+ </sensor>
751
+ </mujoco>
GR00T-WholeBodyControl/gear_sonic/data/robot_model/model_data/g1/meshes/left_wrist_pitch_link.STL ADDED
Binary file (86 kB). View file
 
GR00T-WholeBodyControl/gear_sonic/data/robot_model/model_data/g1/scene_43dof.xml ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <mujoco model="g1_43dof scene">
2
+ <include file="g1_29dof_with_hand.xml"/>
3
+ <!-- Note that the hand model in g1_29dof_with_hand.xml is not stable for simulation. -->
4
+
5
+ <statistic center="0 0 0.5" extent="2.0"/>
6
+
7
+ <visual>
8
+ <headlight diffuse="0.6 0.6 0.6" ambient="0.3 0.3 0.3" specular="0 0 0"/>
9
+ <rgba haze="0.15 0.25 0.35 1"/>
10
+ <global azimuth="-130" elevation="-20"/>
11
+ </visual>
12
+
13
+ <asset>
14
+ <texture type="skybox" builtin="gradient" rgb1="0.3 0.5 0.7" rgb2="0 0 0" width="512" height="3072"/>
15
+ <texture type="2d" name="groundplane" builtin="checker" mark="edge" rgb1="0.2 0.3 0.4" rgb2="0.1 0.2 0.3"
16
+ markrgb="0.8 0.8 0.8" width="300" height="300"/>
17
+ <material name="groundplane" texture="groundplane" texuniform="true" texrepeat="5 5" reflectance="0.2"/>
18
+ </asset>
19
+
20
+ <worldbody>
21
+ <light pos="0 0 1.5" dir="0 0 -1" directional="true"/>
22
+ <geom name="floor" size="0 0 0.05" type="plane" material="groundplane"/>
23
+ <!-- Site -->
24
+ <site name="com_marker" pos="0.1 0 0" size="0.05" rgba="1 0 0 1" type="sphere"/>
25
+ </worldbody>
26
+
27
+ <default>
28
+ <geom friction="1.0"/>
29
+ </default>
30
+ </mujoco>
GR00T-WholeBodyControl/gear_sonic/data/robot_model/robot_model.py ADDED
@@ -0,0 +1,817 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Pinocchio-based robot model with FK, joint limits, gravity compensation, and reduced-DOF views.
2
+
3
+ RobotModel wraps a URDF + optional supplemental info to provide forward kinematics,
4
+ Jacobians, gravity torques, and joint-group queries. ReducedRobotModel maps between
5
+ a full configuration and an actuated-joint subset.
6
+ """
7
+
8
+ from typing import List, Optional, Set, Union
9
+
10
+ import numpy as np
11
+ import pinocchio as pin
12
+
13
+ from gear_sonic.data.robot_model.supplemental_info import RobotSupplementalInfo
14
+
15
+
16
+ class RobotModel:
17
+ def __init__(
18
+ self,
19
+ urdf_path,
20
+ asset_path,
21
+ set_floating_base=False,
22
+ supplemental_info: Optional[RobotSupplementalInfo] = None,
23
+ ):
24
+ self.pinocchio_wrapper = pin.RobotWrapper.BuildFromURDF(
25
+ filename=urdf_path,
26
+ package_dirs=[asset_path],
27
+ root_joint=pin.JointModelFreeFlyer() if set_floating_base else None,
28
+ )
29
+ self.is_floating_base_model = set_floating_base
30
+
31
+ self.joint_to_dof_index = {}
32
+ # Assume we only have single-dof joints
33
+ # First two names correspond to universe and floating base joints
34
+ names = (
35
+ self.pinocchio_wrapper.model.names[2:]
36
+ if set_floating_base
37
+ else self.pinocchio_wrapper.model.names[1:]
38
+ )
39
+ for name in names:
40
+ j_id = self.pinocchio_wrapper.model.getJointId(name)
41
+ jmodel = self.pinocchio_wrapper.model.joints[j_id]
42
+ self.joint_to_dof_index[name] = jmodel.idx_q
43
+
44
+ # Store joint limits only for actual joints (excluding floating base)
45
+ # if set floating base is true and the robot can move in the world
46
+ # then we don't want to impose joint limits for the 7 dofs corresponding
47
+ # to the floating base dofs.
48
+ root_nq = 7 if set_floating_base else 0
49
+ self.upper_joint_limits = self.pinocchio_wrapper.model.upperPositionLimit[root_nq:].copy()
50
+ self.lower_joint_limits = self.pinocchio_wrapper.model.lowerPositionLimit[root_nq:].copy()
51
+
52
+ # Set up supplemental info if provided
53
+ self.supplemental_info = supplemental_info
54
+ if self.supplemental_info is not None:
55
+ # Cache indices for body and hand actuated joints separately
56
+ self._body_actuated_joint_indices = [
57
+ self.dof_index(name) for name in self.supplemental_info.body_actuated_joints
58
+ ]
59
+ self._left_hand_actuated_joint_indices = [
60
+ self.dof_index(name) for name in self.supplemental_info.left_hand_actuated_joints
61
+ ]
62
+ self._right_hand_actuated_joint_indices = [
63
+ self.dof_index(name) for name in self.supplemental_info.right_hand_actuated_joints
64
+ ]
65
+ self._hand_actuated_joint_indices = (
66
+ self._left_hand_actuated_joint_indices + self._right_hand_actuated_joint_indices
67
+ )
68
+
69
+ # Cache indices for joint groups, handling nested groups
70
+ self._joint_group_indices = {}
71
+ for group_name, group_info in self.supplemental_info.joint_groups.items():
72
+ indices = []
73
+ # Add indices for direct joints
74
+ indices.extend([self.dof_index(name) for name in group_info["joints"]])
75
+ # Add indices from subgroups
76
+ for subgroup_name in group_info["groups"]:
77
+ indices.extend(self.get_joint_group_indices(subgroup_name))
78
+ self._joint_group_indices[group_name] = sorted(set(indices))
79
+
80
+ # Update joint limits from supplemental info if available
81
+ if (
82
+ hasattr(self.supplemental_info, "joint_limits")
83
+ and self.supplemental_info.joint_limits
84
+ ):
85
+ for joint_name, limits in self.supplemental_info.joint_limits.items():
86
+ if joint_name in self.joint_to_dof_index:
87
+ # joint_to_dof_index is in full-space (includes floating base DOFs),
88
+ # but limits arrays are indexed from 0 starting at the first real joint
89
+ idx = self.joint_to_dof_index[joint_name] - root_nq
90
+ self.lower_joint_limits[idx] = limits[0]
91
+ self.upper_joint_limits[idx] = limits[1]
92
+
93
+ # Initialize default body pose
94
+ self.default_body_pose = self.q_zero
95
+
96
+ # Update with supplemental info if available
97
+ if self.supplemental_info is not None:
98
+ default_joint_q = self.supplemental_info.default_joint_q
99
+ for joint, joint_values in default_joint_q.items():
100
+ # Get the joint name mapping for this type
101
+ joint_mapping = self.supplemental_info.joint_name_mapping[joint]
102
+
103
+ # Handle both single joint names and left/right mappings
104
+ if isinstance(joint_mapping, str):
105
+ # Single joint (e.g., waist joints)
106
+ if joint_mapping in self.joint_to_dof_index:
107
+ joint_idx = self.dof_index(joint_mapping)
108
+ self.default_body_pose[joint_idx] = (
109
+ joint_values # joint_values is the value for single joints
110
+ )
111
+ else:
112
+ # Left/right mapping (e.g., arm joints)
113
+ for side, value in joint_values.items():
114
+ if side in joint_mapping and joint_mapping[side] in self.joint_to_dof_index:
115
+ joint_idx = self.dof_index(joint_mapping[side])
116
+ self.default_body_pose[joint_idx] = value
117
+
118
+ # Initialize initial body pose
119
+ self.initial_body_pose = self.default_body_pose.copy()
120
+
121
+ @property
122
+ def num_dofs(self) -> int:
123
+ """Get the number of degrees of freedom of the robot (floating base pose + joints)."""
124
+ return self.pinocchio_wrapper.model.nq
125
+
126
+ @property
127
+ def q_zero(self) -> np.ndarray:
128
+ """Get the zero pose of the robot."""
129
+ return self.pinocchio_wrapper.q0.copy()
130
+
131
+ @property
132
+ def joint_names(self) -> List[str]:
133
+ """Get the names of the joints of the robot."""
134
+ return list(self.joint_to_dof_index.keys())
135
+
136
+ @property
137
+ def num_joints(self) -> int:
138
+ """Get the number of joints of the robot."""
139
+ return len(self.joint_to_dof_index)
140
+
141
+ def dof_index(self, joint_name: str) -> int:
142
+ """
143
+ Get the index in the degrees of freedom vector corresponding
144
+ to the single-DoF joint with name `joint_name`.
145
+ """
146
+ if joint_name not in self.joint_to_dof_index:
147
+ raise ValueError(
148
+ f"Unknown joint name: '{joint_name}'. "
149
+ f"Available joints: {list(self.joint_to_dof_index.keys())}"
150
+ )
151
+ return self.joint_to_dof_index[joint_name]
152
+
153
+ def get_body_actuated_joint_indices(self) -> List[int]:
154
+ """
155
+ Get the indices of body actuated joints in the full configuration.
156
+ Ordering is that of the actuated joints as defined in the supplemental info.
157
+ Requires supplemental_info to be provided.
158
+ """
159
+ if self.supplemental_info is None:
160
+ raise ValueError("supplemental_info must be provided to use this method")
161
+ return self._body_actuated_joint_indices
162
+
163
+ def get_hand_actuated_joint_indices(self, side: str = "both") -> List[int]:
164
+ """
165
+ Get the indices of hand actuated joints in the full configuration.
166
+ Ordering is that of the actuated joints as defined in the supplemental info.
167
+ Requires supplemental_info to be provided.
168
+
169
+ Args:
170
+ side: String specifying which hand to get indices for ('left', 'right', or 'both')
171
+ """
172
+ if self.supplemental_info is None:
173
+ raise ValueError("supplemental_info must be provided to use this method")
174
+
175
+ if side.lower() == "both":
176
+ return self._hand_actuated_joint_indices
177
+ elif side.lower() == "left":
178
+ return self._left_hand_actuated_joint_indices
179
+ elif side.lower() == "right":
180
+ return self._right_hand_actuated_joint_indices
181
+ else:
182
+ raise ValueError("side must be 'left', 'right', or 'both'")
183
+
184
+ def get_joint_group_indices(self, group_names: Union[str, Set[str]]) -> List[int]:
185
+ """
186
+ Get the indices of joints in one or more groups in the full configuration.
187
+ Requires supplemental_info to be provided.
188
+ The returned indices are sorted in ascending order, so that the joint ordering
189
+ of the full model is preserved.
190
+
191
+ Args:
192
+ group_names: Either a single group name (str) or a set of group names (Set[str])
193
+
194
+ Returns:
195
+ List of joint indices in sorted order with no duplicates
196
+ """
197
+ if self.supplemental_info is None:
198
+ raise ValueError("supplemental_info must be provided to use this method")
199
+
200
+ # Convert single string to set for uniform handling
201
+ if isinstance(group_names, str):
202
+ group_names = {group_names}
203
+
204
+ # Collect indices from all groups
205
+ all_indices = set()
206
+ for group_name in group_names:
207
+ if group_name not in self._joint_group_indices:
208
+ raise ValueError(f"Unknown joint group: {group_name}")
209
+ all_indices.update(self._joint_group_indices[group_name])
210
+
211
+ return sorted(all_indices)
212
+
213
+ def cache_forward_kinematics(self, q: np.ndarray, auto_clip=True) -> None:
214
+ """
215
+ Perform forward kinematics to update the pose of every joint and frame
216
+ in the Pinocchio data structures for the given configuration `q`.
217
+
218
+ :param q: A numpy array of shape (num_dofs,) representing the robot configuration.
219
+ """
220
+ if q.shape[0] != self.num_dofs:
221
+ raise ValueError(f"Expected q of length {self.num_dofs}, got {q.shape[0]} instead.")
222
+
223
+ # Apply auto-clip if enabled
224
+ if auto_clip:
225
+ q = self.clip_configuration(q)
226
+
227
+ pin.framesForwardKinematics(self.pinocchio_wrapper.model, self.pinocchio_wrapper.data, q)
228
+
229
+ def compute_gravity_compensation_torques(
230
+ self, q: np.ndarray, joint_groups: Union[str, List[str], Set[str]] = None, auto_clip=True
231
+ ) -> np.ndarray:
232
+ """
233
+ Compute gravity compensation torques for specified joint groups using pinocchio.
234
+
235
+ :param q: Robot configuration (joint positions)
236
+ :param joint_groups: Joint groups to compensate (e.g., "arms", ["left_arm", "waist"],
237
+ {"left_arm", "waist"}). If None, compensates all joints
238
+ :param auto_clip: Whether to automatically clip joint values to limits
239
+ :return: Array of gravity compensation torques for all DOFs (zero for non-compensated joints)
240
+ """
241
+ if q.shape[0] != self.num_dofs:
242
+ raise ValueError(f"Expected q of length {self.num_dofs}, got {q.shape[0]} instead.")
243
+
244
+ # Apply auto-clip if enabled
245
+ if auto_clip:
246
+ q = self.clip_configuration(q)
247
+
248
+ try:
249
+ # Cache forward kinematics for the current configuration
250
+ self.cache_forward_kinematics(q, auto_clip=False) # Already clipped if needed
251
+
252
+ # RNEA with zero velocity and acceleration isolates the gravity term:
253
+ # tau = M(q)*0 + C(q,0)*0 + g(q) = g(q), i.e. pure gravity compensation
254
+ v = np.zeros(self.num_dofs)
255
+ a = np.zeros(self.num_dofs)
256
+
257
+ gravity_torques_full = pin.rnea(
258
+ self.pinocchio_wrapper.model, self.pinocchio_wrapper.data, q, v, a
259
+ )
260
+
261
+ # If no joint groups specified, return full gravity torques
262
+ if joint_groups is None:
263
+ return gravity_torques_full
264
+
265
+ # Convert list to set for get_joint_group_indices compatibility
266
+ if isinstance(joint_groups, list):
267
+ joint_groups = set(joint_groups)
268
+
269
+ # Get joint indices for specified groups - get_joint_group_indices handles str and Set[str]
270
+ try:
271
+ compensated_joint_indices = self.get_joint_group_indices(joint_groups)
272
+ except ValueError as e:
273
+ raise ValueError(f"Error resolving joint groups {joint_groups}: {e}")
274
+
275
+ # Create mask for joints that should receive gravity compensation
276
+ compensation_mask = np.zeros(self.num_dofs, dtype=bool)
277
+ for joint_idx in compensated_joint_indices:
278
+ if 0 <= joint_idx < len(compensation_mask):
279
+ compensation_mask[joint_idx] = True
280
+
281
+ # Apply mask to only compensate specified joints
282
+ compensated_torques = np.zeros_like(gravity_torques_full)
283
+ compensated_torques[compensation_mask] = gravity_torques_full[compensation_mask]
284
+
285
+ return compensated_torques
286
+
287
+ except Exception as e:
288
+ raise RuntimeError(f"Error computing gravity compensation: {e}")
289
+
290
+ def clip_configuration(self, q: np.ndarray, margin: float = 1e-6) -> np.ndarray:
291
+ """
292
+ Clip the configuration to stay within joint limits with a small tolerance.
293
+
294
+ :param q: Configuration to clip
295
+ :param margin: Tolerance to keep away from joint limits
296
+ :return: Clipped configuration
297
+ """
298
+ q_clipped = q.copy()
299
+
300
+ # Only clip joint positions, not floating base
301
+ root_nq = 7 if self.is_floating_base_model else 0
302
+ q_clipped[root_nq:] = np.clip(
303
+ q[root_nq:], self.lower_joint_limits + margin, self.upper_joint_limits - margin
304
+ )
305
+
306
+ return q_clipped
307
+
308
+ def frame_placement(self, frame_name: str) -> pin.SE3:
309
+ """
310
+ Returns the SE3 transform of the specified frame in the world coordinate system.
311
+ Note: make sure cache_forward_kinematics() has been previously called.
312
+
313
+ :param frame_name: Name of the frame, e.g. "link_elbow_frame", "hand_imu_frame", etc.
314
+ :return: A pin.SE3 object representing the pose of the frame.
315
+ """
316
+ model = self.pinocchio_wrapper.model
317
+ data = self.pinocchio_wrapper.data
318
+
319
+ frame_id = model.getFrameId(frame_name)
320
+ if frame_id < 0 or frame_id >= len(model.frames):
321
+ valid_frames = [f.name for f in model.frames]
322
+ raise ValueError(f"Unknown frame '{frame_name}'. Valid frames: {valid_frames}")
323
+
324
+ # Pinocchio's data.oMf[frame_id] is a pin.SE3.
325
+ return data.oMf[frame_id].copy()
326
+
327
+ def frame_jacobian(
328
+ self,
329
+ frame_name: str,
330
+ q: np.ndarray,
331
+ reference_frame: pin.ReferenceFrame = pin.LOCAL_WORLD_ALIGNED,
332
+ ) -> np.ndarray:
333
+ """
334
+ Compute the Jacobian of the specified frame.
335
+
336
+ :param frame_name: Name of the frame, e.g. "fingertip_frame", "hand_frame", etc.
337
+ :param q: Configuration vector (joint positions)
338
+ :param reference_frame: Reference frame for the Jacobian. Options:
339
+ - pin.LOCAL: Jacobian expressed in the local frame
340
+ - pin.WORLD: Jacobian expressed in the world frame
341
+ - pin.LOCAL_WORLD_ALIGNED: Local frame with world orientation (default, best for IK)
342
+ :return: A 6xN Jacobian matrix where N is the number of DOFs.
343
+ First 3 rows are linear velocity, last 3 rows are angular velocity.
344
+ """
345
+ model = self.pinocchio_wrapper.model
346
+ data = self.pinocchio_wrapper.data
347
+
348
+ # Get frame ID
349
+ frame_id = model.getFrameId(frame_name)
350
+ if frame_id < 0 or frame_id >= len(model.frames):
351
+ valid_frames = [f.name for f in model.frames]
352
+ raise ValueError(f"Unknown frame '{frame_name}'. Valid frames: {valid_frames}")
353
+
354
+ # Compute the Jacobian
355
+ J = pin.computeFrameJacobian(model, data, q, frame_id, reference_frame)
356
+
357
+ return J.copy()
358
+
359
+ def get_body_actuated_joints(self, q: np.ndarray) -> np.ndarray:
360
+ """
361
+ Get the configuration of body actuated joints from a full configuration.
362
+
363
+ :param q: Configuration in full space
364
+ :return: Configuration of body actuated joints
365
+ """
366
+ indices = self.get_body_actuated_joint_indices()
367
+
368
+ return q[indices]
369
+
370
+ def get_hand_actuated_joints(self, q: np.ndarray, side: str = "both") -> np.ndarray:
371
+ """
372
+ Get the configuration of hand actuated joints from a full configuration.
373
+
374
+ Args:
375
+ q: Configuration in full space
376
+ side: String specifying which hand to get joints for ('left', 'right', or 'both')
377
+ """
378
+ indices = self.get_hand_actuated_joint_indices(side)
379
+ return q[indices]
380
+
381
+ def get_configuration_from_actuated_joints(
382
+ self,
383
+ body_actuated_joint_values: np.ndarray,
384
+ hand_actuated_joint_values: Optional[np.ndarray] = None,
385
+ left_hand_actuated_joint_values: Optional[np.ndarray] = None,
386
+ right_hand_actuated_joint_values: Optional[np.ndarray] = None,
387
+ ) -> np.ndarray:
388
+ """
389
+ Get the full configuration from the body and hand actuated joint configurations.
390
+ Can specify either both hands together or left and right hands separately.
391
+
392
+ Args:
393
+ body_actuated_joint_values: Configuration of body actuated joints
394
+ hand_actuated_joint_values: Configuration of both hands' actuated joints (optional)
395
+ left_hand_actuated_joint_values: Configuration of left hand actuated joints (optional)
396
+ right_hand_actuated_joint_values: Configuration of right hand actuated joints (optional)
397
+
398
+ Returns:
399
+ Full configuration including body and hand joints
400
+ """
401
+ q = self.pinocchio_wrapper.q0.copy()
402
+ q[self.get_body_actuated_joint_indices()] = body_actuated_joint_values
403
+
404
+ # Handle hand configurations
405
+ if hand_actuated_joint_values is not None:
406
+ # Use combined hand configuration
407
+ q[self.get_hand_actuated_joint_indices("both")] = hand_actuated_joint_values
408
+ else:
409
+ # Use separate hand configurations
410
+ if left_hand_actuated_joint_values is not None:
411
+ q[self.get_hand_actuated_joint_indices("left")] = left_hand_actuated_joint_values
412
+ if right_hand_actuated_joint_values is not None:
413
+ q[self.get_hand_actuated_joint_indices("right")] = right_hand_actuated_joint_values
414
+
415
+ return q
416
+
417
+ def reset_forward_kinematics(self) -> None:
418
+ """
419
+ Reset the forward kinematics to the initial configuration.
420
+ """
421
+ self.cache_forward_kinematics(self.q_zero)
422
+
423
+ def get_initial_upper_body_pose(self) -> np.ndarray:
424
+ """
425
+ Get the initial upper body pose of the robot.
426
+ """
427
+ return self.initial_body_pose[self.get_joint_group_indices("upper_body")]
428
+
429
+ def get_default_body_pose(self) -> np.ndarray:
430
+ """
431
+ Get the default body pose of the robot.
432
+ """
433
+ return self.default_body_pose.copy()
434
+
435
+ def set_initial_body_pose(self, q: np.ndarray, q_idx=None) -> None:
436
+ """
437
+ Set the initial body pose of the robot.
438
+ """
439
+ if q_idx is None:
440
+ self.initial_body_pose = q
441
+ else:
442
+ self.initial_body_pose[q_idx] = q
443
+
444
+
445
+ class ReducedRobotModel(RobotModel):
446
+ """
447
+ A class that creates a reduced order robot model by fixing certain joints.
448
+ This class maintains a mapping between the reduced state space and the full state space.
449
+ """
450
+
451
+ def __init__(
452
+ self,
453
+ full_robot_model: RobotModel,
454
+ fixed_joints: List[str],
455
+ fixed_values: Optional[List[float]] = None,
456
+ ):
457
+ """
458
+ Create a reduced order robot model by fixing specified joints.
459
+
460
+ :param full_robot_model: The original robot model
461
+ :param fixed_joints: List of joint names to fix
462
+ :param fixed_values: Optional list of values to fix the joints to. If None, uses the initial
463
+ joint positions (q0) from the full robot model.
464
+ """
465
+ self.full_robot = full_robot_model
466
+ self.supplemental_info = full_robot_model.supplemental_info
467
+
468
+ # If fixed_values is None, use q0 from the full robot model
469
+ if fixed_values is None:
470
+ fixed_values = []
471
+ for joint_name in fixed_joints:
472
+ full_idx = full_robot_model.dof_index(joint_name)
473
+ fixed_values.append(full_robot_model.pinocchio_wrapper.q0[full_idx])
474
+ elif len(fixed_joints) != len(fixed_values):
475
+ raise ValueError("fixed_joints and fixed_values must have the same length")
476
+
477
+ # Store fixed joints and their values
478
+ self.fixed_joints = fixed_joints
479
+ self.fixed_values = fixed_values
480
+
481
+ # reduced_to_full[i] = full-space index of the i-th reduced-space DOF
482
+ # full_to_reduced[j] = reduced-space index of the j-th full-space DOF (active joints only)
483
+ self.reduced_to_full = []
484
+ self.full_to_reduced = {}
485
+
486
+ # Initialize with floating base indices if present
487
+ if full_robot_model.is_floating_base_model:
488
+ self.reduced_to_full.extend(range(7)) # Floating base indices
489
+ for i in range(7):
490
+ self.full_to_reduced[i] = i
491
+
492
+ # Add active joint indices
493
+ for joint_name in full_robot_model.joint_names:
494
+ if joint_name not in fixed_joints:
495
+ full_idx = full_robot_model.dof_index(joint_name)
496
+ reduced_idx = len(self.reduced_to_full)
497
+ self.reduced_to_full.append(full_idx)
498
+ self.full_to_reduced[full_idx] = reduced_idx
499
+
500
+ # Create a reduced Pinocchio model using buildReducedModel
501
+ # First, get the list of joint IDs to lock
502
+ locked_joint_ids = []
503
+ for joint_name in fixed_joints:
504
+ joint_id = full_robot_model.pinocchio_wrapper.model.getJointId(joint_name)
505
+ # Pinocchio reserves id=0 for "universe" and id=1 for floating base (if present).
506
+ # Only lock actual robot joints, not these special entries.
507
+ if (full_robot_model.is_floating_base_model and joint_id > 1) or (
508
+ not full_robot_model.is_floating_base_model and joint_id > 0
509
+ ):
510
+ locked_joint_ids.append(joint_id)
511
+
512
+ # First build the reduced kinematic model
513
+ reduced_model = pin.buildReducedModel(
514
+ full_robot_model.pinocchio_wrapper.model,
515
+ locked_joint_ids,
516
+ full_robot_model.pinocchio_wrapper.q0,
517
+ )
518
+
519
+ # Then build the reduced geometry models using the reduced kinematic model
520
+ self.pinocchio_wrapper = pin.RobotWrapper(
521
+ model=reduced_model,
522
+ )
523
+
524
+ # Create joint to dof index mapping
525
+ self.joint_to_dof_index = {}
526
+ # Assume we only have single-dof joints
527
+ # First two names correspond to universe and floating base joints
528
+ names = (
529
+ self.pinocchio_wrapper.model.names[2:]
530
+ if self.full_robot.is_floating_base_model
531
+ else self.pinocchio_wrapper.model.names[1:]
532
+ )
533
+ for name in names:
534
+ j_id = self.pinocchio_wrapper.model.getJointId(name)
535
+ jmodel = self.pinocchio_wrapper.model.joints[j_id]
536
+ self.joint_to_dof_index[name] = jmodel.idx_q
537
+
538
+ # Initialize joint limits
539
+ root_nq = 7 if self.full_robot.is_floating_base_model else 0
540
+ self.lower_joint_limits = self.pinocchio_wrapper.model.lowerPositionLimit[root_nq:].copy()
541
+ self.upper_joint_limits = self.pinocchio_wrapper.model.upperPositionLimit[root_nq:].copy()
542
+
543
+ # Update joint limits from supplemental info if available
544
+ if self.supplemental_info is not None:
545
+ if (
546
+ hasattr(self.supplemental_info, "joint_limits")
547
+ and self.supplemental_info.joint_limits
548
+ ):
549
+ for joint_name, limits in self.supplemental_info.joint_limits.items():
550
+ if joint_name in self.joint_to_dof_index:
551
+ idx = self.joint_to_dof_index[joint_name] - root_nq
552
+ self.lower_joint_limits[idx] = limits[0]
553
+ self.upper_joint_limits[idx] = limits[1]
554
+
555
+ # Get full indices for body and hand actuated joints
556
+ full_body_indices = full_robot_model.get_body_actuated_joint_indices()
557
+ full_hand_indices = full_robot_model.get_hand_actuated_joint_indices("both")
558
+ full_left_hand_indices = full_robot_model.get_hand_actuated_joint_indices("left")
559
+ full_right_hand_indices = full_robot_model.get_hand_actuated_joint_indices("right")
560
+
561
+ # Map to reduced indices
562
+ self._body_actuated_joint_indices = []
563
+ for idx in full_body_indices:
564
+ if idx in self.full_to_reduced:
565
+ self._body_actuated_joint_indices.append(self.full_to_reduced[idx])
566
+
567
+ self._hand_actuated_joint_indices = []
568
+ for idx in full_hand_indices:
569
+ if idx in self.full_to_reduced:
570
+ self._hand_actuated_joint_indices.append(self.full_to_reduced[idx])
571
+
572
+ self._left_hand_actuated_joint_indices = []
573
+ for idx in full_left_hand_indices:
574
+ if idx in self.full_to_reduced:
575
+ self._left_hand_actuated_joint_indices.append(self.full_to_reduced[idx])
576
+
577
+ self._right_hand_actuated_joint_indices = []
578
+ for idx in full_right_hand_indices:
579
+ if idx in self.full_to_reduced:
580
+ self._right_hand_actuated_joint_indices.append(self.full_to_reduced[idx])
581
+
582
+ # Cache indices for joint groups in reduced space
583
+ self._joint_group_indices = {}
584
+ for group_name in self.supplemental_info.joint_groups:
585
+ full_indices = full_robot_model.get_joint_group_indices(group_name)
586
+ reduced_indices = []
587
+ for idx in full_indices:
588
+ if idx in self.full_to_reduced:
589
+ reduced_indices.append(self.full_to_reduced[idx])
590
+ self._joint_group_indices[group_name] = sorted(set(reduced_indices))
591
+
592
+ # Initialize default body pose in reduced space
593
+ self.default_body_pose = self.full_to_reduced_configuration(
594
+ full_robot_model.default_body_pose
595
+ )
596
+
597
+ # Initialize initial body pose in reduced space
598
+ self.initial_body_pose = self.full_to_reduced_configuration(
599
+ full_robot_model.initial_body_pose
600
+ )
601
+
602
+ @property
603
+ def num_joints(self) -> int:
604
+ """Get the number of active joints in the reduced model."""
605
+ return len(self.joint_names)
606
+
607
+ @property
608
+ def joint_names(self) -> List[str]:
609
+ """Get the names of the active joints in the reduced model."""
610
+ return [name for name in self.full_robot.joint_names if name not in self.fixed_joints]
611
+
612
+ @classmethod
613
+ def from_fixed_groups(
614
+ cls,
615
+ full_robot_model: RobotModel,
616
+ fixed_group_names: List[str],
617
+ fixed_values: Optional[List[float]] = None,
618
+ ) -> "ReducedRobotModel":
619
+ """
620
+ Create a reduced order robot model by fixing all joints in specified groups.
621
+
622
+ :param full_robot_model: The original robot model
623
+ :param fixed_group_names: List of joint group names to fix
624
+ :param fixed_values: Optional list of values to fix the joints to. If None, uses the initial
625
+ joint positions (q0) from the full robot model.
626
+ :return: A ReducedRobotModel instance
627
+ """
628
+ if full_robot_model.supplemental_info is None:
629
+ raise ValueError("supplemental_info must be provided to use this method")
630
+
631
+ # Get all joints in the groups, including those from subgroups
632
+ fixed_joints = set() # Use a set to avoid duplicates
633
+
634
+ for group_name in fixed_group_names:
635
+ if group_name not in full_robot_model.supplemental_info.joint_groups:
636
+ raise ValueError(f"Unknown joint group: {group_name}")
637
+
638
+ group_info = full_robot_model.supplemental_info.joint_groups[group_name]
639
+
640
+ # Add direct joints
641
+ fixed_joints.update(group_info["joints"])
642
+
643
+ # Add joints from subgroups
644
+ for subgroup_name in group_info["groups"]:
645
+ subgroup_joints = full_robot_model.get_joint_group_indices(subgroup_name)
646
+ fixed_joints.update([full_robot_model.joint_names[idx] for idx in subgroup_joints])
647
+
648
+ # Convert set back to list for compatibility with the original constructor
649
+ return cls(full_robot_model, list(fixed_joints), fixed_values)
650
+
651
+ @classmethod
652
+ def from_fixed_group(
653
+ cls,
654
+ full_robot_model: RobotModel,
655
+ fixed_group_name: str,
656
+ fixed_values: Optional[List[float]] = None,
657
+ ) -> "ReducedRobotModel":
658
+ """
659
+ Create a reduced order robot model by fixing all joints in a specified group.
660
+ This is a convenience method that calls from_fixed_groups with a single group.
661
+
662
+ :param full_robot_model: The original robot model
663
+ :param fixed_group_name: Name of the joint group to fix
664
+ :param fixed_values: Optional list of values to fix the joints to. If None, uses the initial
665
+ joint positions (q0) from the full robot model.
666
+ :return: A ReducedRobotModel instance
667
+ """
668
+ return cls.from_fixed_groups(full_robot_model, [fixed_group_name], fixed_values)
669
+
670
+ @classmethod
671
+ def from_active_group(
672
+ cls,
673
+ full_robot_model: RobotModel,
674
+ active_group_name: str,
675
+ fixed_values: Optional[List[float]] = None,
676
+ ) -> "ReducedRobotModel":
677
+ """
678
+ Create a reduced order robot model by fixing all joints EXCEPT those in the specified group.
679
+ This is a convenience method that calls from_active_groups with a single group.
680
+
681
+ :param full_robot_model: The original robot model
682
+ :param active_group_name: Name of the joint group to keep active (all other joints will be fixed)
683
+ :param fixed_values: Optional list of values to fix the joints to. If None, uses the initial
684
+ joint positions (q0) from the full robot model.
685
+ :return: A ReducedRobotModel instance
686
+ """
687
+ return cls.from_active_groups(full_robot_model, [active_group_name], fixed_values)
688
+
689
+ @classmethod
690
+ def from_active_groups(
691
+ cls,
692
+ full_robot_model: RobotModel,
693
+ active_group_names: List[str],
694
+ fixed_values: Optional[List[float]] = None,
695
+ ) -> "ReducedRobotModel":
696
+ """
697
+ Create a reduced order robot model by fixing all joints EXCEPT those in the specified groups.
698
+ This is useful when you want to keep multiple groups active and fix everything else.
699
+
700
+ :param full_robot_model: The original robot model
701
+ :param active_group_names: List of joint group names to keep active (all other joints will be fixed)
702
+ :param fixed_values: Optional list of values to fix the joints to. If None, uses the initial
703
+ joint positions (q0) from the full robot model.
704
+ :return: A ReducedRobotModel instance
705
+ """
706
+ if full_robot_model.supplemental_info is None:
707
+ raise ValueError("supplemental_info must be provided to use this method")
708
+
709
+ # Get all joints in the active groups, including those from subgroups
710
+ active_joints = set()
711
+
712
+ def add_group_joints(group_name: str):
713
+ if group_name not in full_robot_model.supplemental_info.joint_groups:
714
+ raise ValueError(f"Unknown joint group: {group_name}")
715
+
716
+ group_info = full_robot_model.supplemental_info.joint_groups[group_name]
717
+
718
+ # Add direct joints
719
+ if "joints" in group_info:
720
+ active_joints.update(group_info["joints"])
721
+
722
+ # Add joints from subgroups
723
+ if "groups" in group_info:
724
+ for subgroup_name in group_info["groups"]:
725
+ add_group_joints(subgroup_name)
726
+
727
+ for group_name in active_group_names:
728
+ add_group_joints(group_name)
729
+
730
+ # Get all joints from the model
731
+ all_joints = set(full_robot_model.joint_names)
732
+
733
+ # The fixed joints are all joints minus the active joints
734
+ fixed_joints = list(all_joints - active_joints)
735
+
736
+ return cls(full_robot_model, fixed_joints, fixed_values)
737
+
738
+ def reduced_to_full_configuration(self, q_reduced: np.ndarray) -> np.ndarray:
739
+ """
740
+ Convert a reduced configuration to the full configuration space.
741
+
742
+ :param q_reduced: Configuration in reduced space
743
+ :return: Configuration in full space with fixed joints set to their fixed values
744
+ """
745
+ if q_reduced.shape[0] != self.num_dofs:
746
+ raise ValueError(
747
+ f"Expected q_reduced of length {self.num_dofs}, got {q_reduced.shape[0]} instead"
748
+ )
749
+
750
+ q_full = np.zeros(self.full_robot.num_dofs)
751
+
752
+ # Set active joints
753
+ for reduced_idx, full_idx in enumerate(self.reduced_to_full):
754
+ q_full[full_idx] = q_reduced[reduced_idx]
755
+
756
+ # Set fixed joints
757
+ for joint_name, value in zip(self.fixed_joints, self.fixed_values):
758
+ full_idx = self.full_robot.dof_index(joint_name)
759
+ q_full[full_idx] = value
760
+
761
+ return q_full
762
+
763
+ def full_to_reduced_configuration(self, q_full: np.ndarray) -> np.ndarray:
764
+ """
765
+ Convert a full configuration to the reduced configuration space.
766
+
767
+ :param q_full: Configuration in full space
768
+ :return: Configuration in reduced space
769
+ """
770
+ if q_full.shape[0] != self.full_robot.num_dofs:
771
+ raise ValueError(
772
+ f"Expected q_full of length {self.full_robot.num_dofs}, got {q_full.shape[0]} instead"
773
+ )
774
+
775
+ q_reduced = np.zeros(self.num_dofs)
776
+
777
+ # Copy active joints
778
+ for reduced_idx, full_idx in enumerate(self.reduced_to_full):
779
+ q_reduced[reduced_idx] = q_full[full_idx]
780
+
781
+ return q_reduced
782
+
783
+ def cache_forward_kinematics(self, q_reduced: np.ndarray, auto_clip=True) -> None:
784
+ """
785
+ Perform forward kinematics using the reduced configuration.
786
+
787
+ :param q_reduced: Configuration in reduced space
788
+ """
789
+ # First update the full robot's forward kinematics
790
+ q_full = self.reduced_to_full_configuration(q_reduced)
791
+ self.full_robot.cache_forward_kinematics(q_full, auto_clip)
792
+
793
+ # Then update the reduced model's forward kinematics
794
+ pin.framesForwardKinematics(
795
+ self.pinocchio_wrapper.model, self.pinocchio_wrapper.data, q_reduced
796
+ )
797
+
798
+ def clip_configuration(self, q_reduced: np.ndarray, margin: float = 1e-6) -> np.ndarray:
799
+ """
800
+ Clip the reduced configuration to stay within joint limits with a small tolerance.
801
+
802
+ :param q_reduced: Configuration to clip
803
+ :param margin: Tolerance to keep away from joint limits
804
+ :return: Clipped configuration
805
+ """
806
+ q_full = self.reduced_to_full_configuration(q_reduced)
807
+ q_full_clipped = self.full_robot.clip_configuration(q_full, margin)
808
+ return self.full_to_reduced_configuration(q_full_clipped)
809
+
810
+ def reset_forward_kinematics(self):
811
+ """
812
+ Reset the forward kinematics to the initial configuration.
813
+ """
814
+ # Reset full robot's forward kinematics
815
+ self.full_robot.reset_forward_kinematics()
816
+ # Reset reduced model's forward kinematics
817
+ self.cache_forward_kinematics(self.q_zero)
GR00T-WholeBodyControl/gear_sonic/data/robot_model/supplemental_info/__init__.py ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ """Robot supplemental info: non-URDF metadata (joint groups, limits, name maps)."""
2
+
3
+ from gear_sonic.data.robot_model.supplemental_info.robot_supplemental_info import (
4
+ RobotSupplementalInfo,
5
+ )
6
+
7
+ __all__ = ["RobotSupplementalInfo"]
GR00T-WholeBodyControl/gear_sonic/data/robot_model/supplemental_info/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (419 Bytes). View file
 
GR00T-WholeBodyControl/gear_sonic/data/robot_model/supplemental_info/__pycache__/robot_supplemental_info.cpython-310.pyc ADDED
Binary file (1.4 kB). View file
 
GR00T-WholeBodyControl/gear_sonic/data/robot_model/supplemental_info/g1/__init__.py ADDED
File without changes
GR00T-WholeBodyControl/gear_sonic/data/robot_model/supplemental_info/g1/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (191 Bytes). View file
 
GR00T-WholeBodyControl/gear_sonic/data/robot_model/supplemental_info/g1/__pycache__/g1_supplemental_info.cpython-310.pyc ADDED
Binary file (5.74 kB). View file
 
GR00T-WholeBodyControl/gear_sonic/data/robot_model/supplemental_info/g1/g1_supplemental_info.py ADDED
@@ -0,0 +1,334 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """G1-specific supplemental info: actuated joints, limits, and default poses by waist/elbow config."""
2
+
3
+ from dataclasses import dataclass
4
+ from enum import Enum
5
+
6
+ import numpy as np
7
+
8
+ from gear_sonic.data.robot_model.supplemental_info.robot_supplemental_info import (
9
+ RobotSupplementalInfo,
10
+ )
11
+
12
+
13
+ class WaistLocation(Enum):
14
+ """Enum for waist location configuration."""
15
+
16
+ LOWER_BODY = "lower_body"
17
+ UPPER_BODY = "upper_body"
18
+ LOWER_AND_UPPER_BODY = "lower_and_upper_body"
19
+
20
+
21
+ class ElbowPose(Enum):
22
+ """Enum for elbow pose configuration."""
23
+
24
+ LOW = "low"
25
+ HIGH = "high"
26
+
27
+
28
+ @dataclass
29
+ class G1SupplementalInfo(RobotSupplementalInfo):
30
+ """
31
+ Supplemental information for the G1 robot.
32
+
33
+ Args:
34
+ waist_location: Where to place waist joints in the joint groups
35
+ elbow_pose: Which elbow pose configuration to use for default joint positions
36
+ """
37
+
38
+ def __init__(
39
+ self,
40
+ waist_location: WaistLocation = WaistLocation.LOWER_BODY,
41
+ elbow_pose: ElbowPose = ElbowPose.LOW,
42
+ ):
43
+ name = "G1_G1ThreeFinger"
44
+
45
+ # Define all actuated joints
46
+ body_actuated_joints = [
47
+ # Left leg
48
+ "left_hip_pitch_joint",
49
+ "left_hip_roll_joint",
50
+ "left_hip_yaw_joint",
51
+ "left_knee_joint",
52
+ "left_ankle_pitch_joint",
53
+ "left_ankle_roll_joint",
54
+ # Right leg
55
+ "right_hip_pitch_joint",
56
+ "right_hip_roll_joint",
57
+ "right_hip_yaw_joint",
58
+ "right_knee_joint",
59
+ "right_ankle_pitch_joint",
60
+ "right_ankle_roll_joint",
61
+ # Waist
62
+ "waist_yaw_joint",
63
+ "waist_roll_joint",
64
+ "waist_pitch_joint",
65
+ # Left arm
66
+ "left_shoulder_pitch_joint",
67
+ "left_shoulder_roll_joint",
68
+ "left_shoulder_yaw_joint",
69
+ "left_elbow_joint",
70
+ "left_wrist_roll_joint",
71
+ "left_wrist_pitch_joint",
72
+ "left_wrist_yaw_joint",
73
+ # Right arm
74
+ "right_shoulder_pitch_joint",
75
+ "right_shoulder_roll_joint",
76
+ "right_shoulder_yaw_joint",
77
+ "right_elbow_joint",
78
+ "right_wrist_roll_joint",
79
+ "right_wrist_pitch_joint",
80
+ "right_wrist_yaw_joint",
81
+ ]
82
+
83
+ left_hand_actuated_joints = [
84
+ # Left hand
85
+ "left_hand_thumb_0_joint",
86
+ "left_hand_thumb_1_joint",
87
+ "left_hand_thumb_2_joint",
88
+ "left_hand_index_0_joint",
89
+ "left_hand_index_1_joint",
90
+ "left_hand_middle_0_joint",
91
+ "left_hand_middle_1_joint",
92
+ ]
93
+
94
+ right_hand_actuated_joints = [
95
+ # Right hand
96
+ "right_hand_thumb_0_joint",
97
+ "right_hand_thumb_1_joint",
98
+ "right_hand_thumb_2_joint",
99
+ "right_hand_index_0_joint",
100
+ "right_hand_index_1_joint",
101
+ "right_hand_middle_0_joint",
102
+ "right_hand_middle_1_joint",
103
+ ]
104
+
105
+ # Define joint limits from URDF
106
+ joint_limits = {
107
+ # Left leg
108
+ "left_hip_pitch_joint": [-2.5307, 2.8798],
109
+ "left_hip_roll_joint": [-0.5236, 2.9671],
110
+ "left_hip_yaw_joint": [-2.7576, 2.7576],
111
+ "left_knee_joint": [-0.087267, 2.8798],
112
+ "left_ankle_pitch_joint": [-0.87267, 0.5236],
113
+ "left_ankle_roll_joint": [-0.2618, 0.2618],
114
+ # Right leg
115
+ "right_hip_pitch_joint": [-2.5307, 2.8798],
116
+ "right_hip_roll_joint": [-2.9671, 0.5236],
117
+ "right_hip_yaw_joint": [-2.7576, 2.7576],
118
+ "right_knee_joint": [-0.087267, 2.8798],
119
+ "right_ankle_pitch_joint": [-0.87267, 0.5236],
120
+ "right_ankle_roll_joint": [-0.2618, 0.2618],
121
+ # Waist
122
+ "waist_yaw_joint": [-2.618, 2.618],
123
+ "waist_roll_joint": [-0.52, 0.52],
124
+ "waist_pitch_joint": [-0.52, 0.52],
125
+ # Left arm
126
+ "left_shoulder_pitch_joint": [-3.0892, 2.6704],
127
+ "left_shoulder_roll_joint": [0.19, 2.2515],
128
+ "left_shoulder_yaw_joint": [-2.618, 2.618],
129
+ "left_elbow_joint": [-1.0472, 2.0944],
130
+ "left_wrist_roll_joint": [-1.972222054, 1.972222054],
131
+ "left_wrist_pitch_joint": [-1.614429558, 1.614429558],
132
+ "left_wrist_yaw_joint": [-1.614429558, 1.614429558],
133
+ # Right arm
134
+ "right_shoulder_pitch_joint": [-3.0892, 2.6704],
135
+ "right_shoulder_roll_joint": [-2.2515, -0.19],
136
+ "right_shoulder_yaw_joint": [-2.618, 2.618],
137
+ "right_elbow_joint": [-1.0472, 2.0944],
138
+ "right_wrist_roll_joint": [-1.972222054, 1.972222054],
139
+ "right_wrist_pitch_joint": [-1.614429558, 1.614429558],
140
+ "right_wrist_yaw_joint": [-1.614429558, 1.614429558],
141
+ # Left hand
142
+ "left_hand_thumb_0_joint": [-1.04719755, 1.04719755],
143
+ "left_hand_thumb_1_joint": [-0.72431163, 1.04719755],
144
+ "left_hand_thumb_2_joint": [0, 1.74532925],
145
+ "left_hand_index_0_joint": [-1.57079632, 0],
146
+ "left_hand_index_1_joint": [-1.74532925, 0],
147
+ "left_hand_middle_0_joint": [-1.57079632, 0],
148
+ "left_hand_middle_1_joint": [-1.74532925, 0],
149
+ # Right hand
150
+ "right_hand_thumb_0_joint": [-1.04719755, 1.04719755],
151
+ "right_hand_thumb_1_joint": [-0.72431163, 1.04719755],
152
+ "right_hand_thumb_2_joint": [0, 1.74532925],
153
+ "right_hand_index_0_joint": [-1.57079632, 0],
154
+ "right_hand_index_1_joint": [-1.74532925, 0],
155
+ "right_hand_middle_0_joint": [-1.57079632, 0],
156
+ "right_hand_middle_1_joint": [-1.74532925, 0],
157
+ }
158
+
159
+ # Define joint groups
160
+ joint_groups = {
161
+ # Body groups
162
+ "waist": {
163
+ "joints": ["waist_yaw_joint", "waist_roll_joint", "waist_pitch_joint"],
164
+ "groups": [],
165
+ },
166
+ # Leg groups
167
+ "left_leg": {
168
+ "joints": [
169
+ "left_hip_pitch_joint",
170
+ "left_hip_roll_joint",
171
+ "left_hip_yaw_joint",
172
+ "left_knee_joint",
173
+ "left_ankle_pitch_joint",
174
+ "left_ankle_roll_joint",
175
+ ],
176
+ "groups": [],
177
+ },
178
+ "right_leg": {
179
+ "joints": [
180
+ "right_hip_pitch_joint",
181
+ "right_hip_roll_joint",
182
+ "right_hip_yaw_joint",
183
+ "right_knee_joint",
184
+ "right_ankle_pitch_joint",
185
+ "right_ankle_roll_joint",
186
+ ],
187
+ "groups": [],
188
+ },
189
+ "legs": {"joints": [], "groups": ["left_leg", "right_leg"]},
190
+ # Arm groups
191
+ "left_arm": {
192
+ "joints": [
193
+ "left_shoulder_pitch_joint",
194
+ "left_shoulder_roll_joint",
195
+ "left_shoulder_yaw_joint",
196
+ "left_elbow_joint",
197
+ "left_wrist_roll_joint",
198
+ "left_wrist_pitch_joint",
199
+ "left_wrist_yaw_joint",
200
+ ],
201
+ "groups": [],
202
+ },
203
+ "right_arm": {
204
+ "joints": [
205
+ "right_shoulder_pitch_joint",
206
+ "right_shoulder_roll_joint",
207
+ "right_shoulder_yaw_joint",
208
+ "right_elbow_joint",
209
+ "right_wrist_roll_joint",
210
+ "right_wrist_pitch_joint",
211
+ "right_wrist_yaw_joint",
212
+ ],
213
+ "groups": [],
214
+ },
215
+ "arms": {"joints": [], "groups": ["left_arm", "right_arm"]},
216
+ # Hand groups
217
+ "left_hand": {
218
+ "joints": [
219
+ "left_hand_index_0_joint",
220
+ "left_hand_index_1_joint",
221
+ "left_hand_middle_0_joint",
222
+ "left_hand_middle_1_joint",
223
+ "left_hand_thumb_0_joint",
224
+ "left_hand_thumb_1_joint",
225
+ "left_hand_thumb_2_joint",
226
+ ],
227
+ "groups": [],
228
+ },
229
+ "right_hand": {
230
+ "joints": [
231
+ "right_hand_index_0_joint",
232
+ "right_hand_index_1_joint",
233
+ "right_hand_middle_0_joint",
234
+ "right_hand_middle_1_joint",
235
+ "right_hand_thumb_0_joint",
236
+ "right_hand_thumb_1_joint",
237
+ "right_hand_thumb_2_joint",
238
+ ],
239
+ "groups": [],
240
+ },
241
+ "hands": {"joints": [], "groups": ["left_hand", "right_hand"]},
242
+ # Full body groups
243
+ "lower_body": {"joints": [], "groups": ["waist", "legs"]},
244
+ "upper_body_no_hands": {"joints": [], "groups": ["arms"]},
245
+ "body": {"joints": [], "groups": ["lower_body", "upper_body_no_hands"]},
246
+ "upper_body": {"joints": [], "groups": ["upper_body_no_hands", "hands"]},
247
+ }
248
+
249
+ # Define joint name mapping from generic types to robot-specific names
250
+ joint_name_mapping = {
251
+ # Waist joints
252
+ "waist_pitch": "waist_pitch_joint",
253
+ "waist_roll": "waist_roll_joint",
254
+ "waist_yaw": "waist_yaw_joint",
255
+ # Shoulder joints
256
+ "shoulder_pitch": {
257
+ "left": "left_shoulder_pitch_joint",
258
+ "right": "right_shoulder_pitch_joint",
259
+ },
260
+ "shoulder_roll": {
261
+ "left": "left_shoulder_roll_joint",
262
+ "right": "right_shoulder_roll_joint",
263
+ },
264
+ "shoulder_yaw": {
265
+ "left": "left_shoulder_yaw_joint",
266
+ "right": "right_shoulder_yaw_joint",
267
+ },
268
+ # Elbow joints
269
+ "elbow_pitch": {"left": "left_elbow_joint", "right": "right_elbow_joint"},
270
+ # Wrist joints
271
+ "wrist_pitch": {"left": "left_wrist_pitch_joint", "right": "right_wrist_pitch_joint"},
272
+ "wrist_roll": {"left": "left_wrist_roll_joint", "right": "right_wrist_roll_joint"},
273
+ "wrist_yaw": {"left": "left_wrist_yaw_joint", "right": "right_wrist_yaw_joint"},
274
+ }
275
+
276
+ root_frame_name = "pelvis"
277
+
278
+ hand_frame_names = {"left": "left_wrist_yaw_link", "right": "right_wrist_yaw_link"}
279
+
280
+ calibration_joint_q = {"elbow_pitch": {"left": 0.0, "right": 0.0}}
281
+
282
+ # 90° Y-axis rotation: aligns hand-tracking frame (palm-forward) to robot wrist frame
283
+ hand_rotation_correction = np.array([[0, 0, 1], [0, 1, 0], [-1, 0, 0]])
284
+
285
+ # HIGH: arms raised with elbows bent (hands near shoulder height)
286
+ # LOW: arms relaxed at sides with slight shoulder roll
287
+ if elbow_pose == ElbowPose.HIGH:
288
+ default_joint_q = {
289
+ "shoulder_roll": {"left": 0.5, "right": -0.5},
290
+ "shoulder_pitch": {"left": -0.2, "right": -0.2},
291
+ "shoulder_yaw": {"left": -0.5, "right": 0.5},
292
+ "wrist_roll": {"left": -0.5, "right": 0.5},
293
+ "wrist_yaw": {"left": 0.5, "right": -0.5},
294
+ "wrist_pitch": {"left": -0.2, "right": -0.2},
295
+ }
296
+ else: # ElbowPose.LOW
297
+ default_joint_q = {
298
+ "shoulder_roll": {"left": 0.2, "right": -0.2},
299
+ }
300
+
301
+ teleop_upper_body_motion_scale = 1.0
302
+
303
+ # Configure joint groups based on waist location
304
+ modified_joint_groups = joint_groups.copy()
305
+ if waist_location == WaistLocation.UPPER_BODY:
306
+ # Move waist from lower_body to upper_body_no_hands
307
+ modified_joint_groups["lower_body"] = {"joints": [], "groups": ["legs"]}
308
+ modified_joint_groups["upper_body_no_hands"] = {
309
+ "joints": [],
310
+ "groups": ["arms", "waist"],
311
+ }
312
+ elif waist_location == WaistLocation.LOWER_AND_UPPER_BODY:
313
+ # Add waist to upper_body_no_hands while keeping it in lower_body
314
+ modified_joint_groups["upper_body_no_hands"] = {
315
+ "joints": [],
316
+ "groups": ["arms", "waist"],
317
+ }
318
+ # For LOWER_BODY, keep default joint_groups as is
319
+
320
+ super().__init__(
321
+ name=name,
322
+ body_actuated_joints=body_actuated_joints,
323
+ left_hand_actuated_joints=left_hand_actuated_joints,
324
+ right_hand_actuated_joints=right_hand_actuated_joints,
325
+ joint_limits=joint_limits,
326
+ joint_groups=modified_joint_groups,
327
+ root_frame_name=root_frame_name,
328
+ hand_frame_names=hand_frame_names,
329
+ calibration_joint_q=calibration_joint_q,
330
+ joint_name_mapping=joint_name_mapping,
331
+ hand_rotation_correction=hand_rotation_correction,
332
+ default_joint_q=default_joint_q,
333
+ teleop_upper_body_motion_scale=teleop_upper_body_motion_scale,
334
+ )
GR00T-WholeBodyControl/gear_sonic/data/robot_model/supplemental_info/robot_supplemental_info.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Base dataclass for robot-specific config not found in URDF (joint groups, limits, names)."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Dict, List, Mapping, Union
5
+
6
+ import numpy as np
7
+
8
+
9
+ @dataclass
10
+ class RobotSupplementalInfo:
11
+ """
12
+ Base class for robot-specific information that is not easily extractable from URDF.
13
+ This includes information about actuated joints, joint hierarchies, etc.
14
+ """
15
+
16
+ name: str
17
+
18
+ # List of body actuated joint names (excluding hands)
19
+ body_actuated_joints: List[str]
20
+
21
+ # List of left hand actuated joint names
22
+ left_hand_actuated_joints: List[str]
23
+
24
+ # List of right hand actuated joint names
25
+ right_hand_actuated_joints: List[str]
26
+
27
+ # Dictionary of joint groups, where each group is a dictionary with:
28
+ # - "joints": list of joint names
29
+ # - "groups": list of subgroup names (optional)
30
+ # Example: {
31
+ # "right_arm": {
32
+ # "joints": ["right_shoulder_pitch_joint", "right_shoulder_roll_joint", "right_elbow_joint"],
33
+ # "groups": []
34
+ # },
35
+ # "left_arm": {
36
+ # "joints": ["left_shoulder_pitch_joint", "left_shoulder_roll_joint", "left_elbow_joint"],
37
+ # "groups": []
38
+ # },
39
+ # "upper_body": {
40
+ # "joints": ["torso_pitch_joint", "torso_yaw_joint", "torso_roll_joint"],
41
+ # "groups": ["right_arm", "left_arm"]
42
+ # }
43
+ # }
44
+ joint_groups: Dict[str, Dict[str, List[str]]]
45
+
46
+ # Name of the root frame
47
+ root_frame_name: str
48
+
49
+ # Dictionary of hand frame names
50
+ # Example: {
51
+ # "left": "left_hand_frame",
52
+ # "right": "right_hand_frame"
53
+ # }
54
+ hand_frame_names: Dict[str, str]
55
+
56
+ # Dictionary of joint limits
57
+ # Example: {
58
+ # "left_shoulder_pitch_joint": [-np.pi / 2, np.pi / 2],
59
+ # "right_shoulder_pitch_joint": [-np.pi / 2, np.pi / 2]
60
+ # }
61
+ joint_limits: Dict[str, List[float]]
62
+
63
+ # Dictionary of calibration joint positions in radians.
64
+ # Structure mirrors default_joint_q for any joints used in calibration.
65
+ # Example: {
66
+ # "elbow_pitch": {"left": -np.pi / 2, "right": -np.pi / 2}
67
+ # }
68
+ calibration_joint_q: Mapping[str, Union[float, Mapping[str, float]]]
69
+
70
+ # Dictionary of joint name mapping from generic types to robot-specific names
71
+ # Example: {
72
+ # "waist_pitch": "waist_pitch_joint",
73
+ # "shoulder_pitch": {
74
+ # "left": "left_shoulder_pitch_joint",
75
+ # "right": "right_shoulder_pitch_joint"
76
+ # },
77
+ # "elbow_pitch": {
78
+ # "left": "left_elbow_pitch_joint",
79
+ # "right": "right_elbow_pitch_joint"
80
+ # }
81
+ # }
82
+ joint_name_mapping: Mapping[str, Union[str, Mapping[str, str]]]
83
+
84
+ # Maps from generic joint names to robot-specific joint values
85
+ # Example: {
86
+ # "waist_roll": 0.2,
87
+ # "elbow_pitch": {"left": 1.0, "right": 1.0}
88
+ # }
89
+ default_joint_q: Mapping[str, Union[float, Mapping[str, float]]]
90
+
91
+ hand_rotation_correction: np.ndarray
92
+
93
+ teleop_upper_body_motion_scale: float
GR00T-WholeBodyControl/gear_sonic/eval_agent_trl.py ADDED
@@ -0,0 +1,670 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # Copyright 2025 The HuggingFace Team. All rights reserved.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ try:
17
+ import isaaclab # noqa: F401
18
+ except ImportError:
19
+ print(
20
+ "\n"
21
+ "ERROR: Isaac Lab is required for evaluation but not installed.\n"
22
+ "\n"
23
+ "Isaac Lab is not a pip dependency — it must be installed separately.\n"
24
+ "Follow the official guide:\n"
25
+ " https://isaac-sim.github.io/IsaacLab/main/source/setup/installation/index.html\n"
26
+ "\n"
27
+ "After installing, activate the Isaac Lab conda/venv environment\n"
28
+ "before running this script.\n"
29
+ )
30
+ import sys
31
+ sys.exit(1)
32
+
33
+ import filelock # noqa: I001
34
+ import json
35
+ import os
36
+ import shutil
37
+ import subprocess
38
+ import sys
39
+
40
+ sys.path.append(os.getcwd())
41
+ import logging
42
+ from pathlib import Path
43
+
44
+ import easydict
45
+ import hydra
46
+ from hydra import utils
47
+ from hydra.core import hydra_config
48
+ from loguru import logger
49
+ import omegaconf
50
+ import yaml
51
+
52
+ from gear_sonic import train_agent_trl
53
+ from gear_sonic.trl.utils import common as trl_utils_common
54
+ from gear_sonic.trl.utils import scheduler
55
+ from gear_sonic.utils import common as rl_utils_common
56
+ from gear_sonic.utils import config_utils, obs_utils
57
+
58
+ config_utils.register_rl_resolvers()
59
+
60
+
61
+ @hydra.main(config_path="config", config_name="base_eval")
62
+ def main(override_config: omegaconf.OmegaConf):
63
+
64
+ hydra_log_path = os.path.join(hydra_config.HydraConfig.get().runtime.output_dir, "eval.log")
65
+ logger.remove()
66
+ logger.add(hydra_log_path, level="DEBUG")
67
+
68
+ # Get log level from LOGURU_LEVEL environment variable or use INFO as default
69
+ console_log_level = os.environ.get("LOGURU_LEVEL", "INFO").upper()
70
+ logger.add(sys.stdout, level=console_log_level, colorize=True)
71
+
72
+ from gear_sonic.utils import logging as utils_logging
73
+
74
+ logging.basicConfig(level=logging.DEBUG)
75
+ logging.getLogger().addHandler(utils_logging.HydraLoggerBridge())
76
+
77
+ os.chdir(hydra.utils.get_original_cwd())
78
+
79
+ if override_config.checkpoint is not None:
80
+ has_config = True
81
+ checkpoint = Path(override_config.checkpoint)
82
+ config_path = checkpoint.parent / "config.yaml"
83
+ if not config_path.exists():
84
+ config_path = checkpoint.parent.parent / "config.yaml"
85
+ if not config_path.exists():
86
+ has_config = False
87
+ logger.error(f"Could not find config path: {config_path}")
88
+
89
+ if has_config:
90
+ logger.info(f"Loading training config file from {config_path}")
91
+ with open(config_path) as file:
92
+ raw = file.read()
93
+ # Backward compatibility: rewrite internal repo module paths to release repo paths
94
+ raw = raw.replace("groot.rl.trl.", "gear_sonic.trl.")
95
+ raw = raw.replace("groot.rl.envs.", "gear_sonic.envs.")
96
+ raw = raw.replace("groot.rl.utils.", "gear_sonic.utils.")
97
+ raw = raw.replace("groot.rl.agents.modules.modules.", "gear_sonic.trl.modules.base_module.")
98
+ raw = raw.replace("groot.rl.agents.", "gear_sonic.trl.")
99
+ raw = raw.replace("groot/rl/data/", "gear_sonic/data/")
100
+ raw = raw.replace("assets/bm/unitree_description/", "assets/robot_description/")
101
+ raw = raw.replace("1215_bones_seed_filtered", "bones_seed_smpl")
102
+ import io
103
+ train_config = omegaconf.OmegaConf.load(io.StringIO(raw))
104
+
105
+ if train_config.eval_overrides is not None:
106
+ train_config = omegaconf.OmegaConf.merge(train_config, train_config.eval_overrides)
107
+
108
+ config = omegaconf.OmegaConf.merge(train_config, override_config)
109
+ else:
110
+ config = override_config
111
+
112
+ config.experiment_dir = checkpoint.parent
113
+ elif override_config.eval_overrides is not None:
114
+ config = override_config.copy()
115
+ eval_overrides = omegaconf.OmegaConf.to_container(config.eval_overrides, resolve=True)
116
+ for arg in sys.argv[1:]:
117
+ if not arg.startswith("+"):
118
+ key = arg.split("=")[0]
119
+ if key in eval_overrides:
120
+ del eval_overrides[key]
121
+ config.eval_overrides = omegaconf.OmegaConf.create(eval_overrides)
122
+ config = omegaconf.OmegaConf.merge(config, eval_overrides)
123
+ else:
124
+ config = override_config
125
+
126
+ meta_path = Path(config.experiment_dir) / "meta.yaml"
127
+ if meta_path.exists():
128
+ meta = yaml.safe_load(open(meta_path)) # noqa: SIM115
129
+ if config.get("wandb", None) is not None and meta.get("wandb_run"):
130
+ config.wandb.wandb_id = meta["wandb_run"]
131
+ print(f"resume wandb from run: {config.wandb.wandb_id}") # noqa: T201
132
+
133
+ with omegaconf.open_dict(config):
134
+ for event in config.manager_env.config.get("train_only_events", []):
135
+ if event in config.manager_env.events:
136
+ config.manager_env.events.pop(event)
137
+ remove_schedule_keys = []
138
+ for key in config.trainer.get("schedule_dict", {}):
139
+ if event in key:
140
+ remove_schedule_keys.append(key)
141
+ for key in remove_schedule_keys:
142
+ config.trainer.schedule_dict.pop(key)
143
+
144
+ for termination in config.manager_env.config.get("train_only_terminations", []):
145
+ if termination in config.manager_env.terminations:
146
+ config.manager_env.terminations.pop(termination)
147
+
148
+ use_encoder = config.get("use_encoder", None)
149
+ if use_encoder is not None:
150
+ encoder_sample_probs = config.manager_env.commands.motion.encoder_sample_probs
151
+ if encoder_sample_probs is not None:
152
+ for encoder in encoder_sample_probs:
153
+ if encoder != use_encoder:
154
+ encoder_sample_probs[encoder] = 0.0
155
+ print(f"Using encoder: {use_encoder}") # noqa: T201
156
+ print(f"Encoder sample probs: {encoder_sample_probs}") # noqa: T201
157
+
158
+ simulator_type = "IsaacSim"
159
+ env_config = config.manager_env
160
+
161
+ import datetime as dt
162
+
163
+ import accelerate
164
+ import torch # noqa: E402, RUF100
165
+
166
+ kwargs = accelerate.InitProcessGroupKwargs(timeout=dt.timedelta(seconds=6000))
167
+ accelerator = accelerate.Accelerator(kwargs_handlers=[kwargs])
168
+
169
+ device = str(accelerator.device)
170
+ if accelerator.device.type == "cuda":
171
+ try:
172
+ torch.cuda.set_device(accelerator.local_process_index)
173
+ except Exception: # noqa: S110, BLE001
174
+ pass
175
+
176
+ device = str(accelerator.device)
177
+ config.multi_gpu = accelerator.num_processes > 1
178
+ if config.multi_gpu:
179
+ config.global_rank = accelerator.process_index
180
+ config.seed += accelerator.process_index
181
+ config.algo.config.global_rank = accelerator.process_index
182
+ config.algo.config.world_size = accelerator.num_processes
183
+ rl_utils_common.seeding(config.seed)
184
+
185
+ def _pick_display_gpu_index(default_idx: int = 0) -> int:
186
+ try:
187
+ out = subprocess.check_output(
188
+ ["nvidia-smi", "--query-gpu=index,display_active,name", "--format=csv,noheader"],
189
+ text=True,
190
+ )
191
+ for line in out.strip().splitlines():
192
+ parts = [p.strip() for p in line.split(",")]
193
+ if len(parts) >= 2:
194
+ idx, active = int(parts[0]), parts[1].lower()
195
+ if active.startswith("enabled") or active.startswith("on"):
196
+ return idx
197
+ except Exception: # noqa: S110, BLE001
198
+ pass
199
+ return default_idx
200
+
201
+ render_gpu_idx = _pick_display_gpu_index(default_idx=0)
202
+
203
+ if simulator_type == "IsaacSim":
204
+ try:
205
+ with open("./rl/simulator/isaacsim/.isaacsim_version", encoding="utf-8") as f:
206
+ DEFAULT_ISAACSIM_VERSION = f.read().strip()
207
+ except FileNotFoundError:
208
+ DEFAULT_ISAACSIM_VERSION = "4.5"
209
+
210
+ if DEFAULT_ISAACSIM_VERSION == "4.5":
211
+ from isaaclab.app import AppLauncher
212
+ elif DEFAULT_ISAACSIM_VERSION == "4.2":
213
+ logger.warning("Using IsaacSim 4.2, replacing isaaclab with omni.isaac.lab")
214
+ from omni.isaac.lab.app import AppLauncher # 4.2
215
+ import argparse
216
+
217
+ parser = argparse.ArgumentParser(description="Evaluate an RL agent with TRL.")
218
+ AppLauncher.add_app_launcher_args(parser)
219
+
220
+ args_cli, hydra_args = parser.parse_known_args()
221
+ sys.argv = [sys.argv[0]] + hydra_args # noqa: RUF005
222
+ args_cli.num_envs = config.num_envs
223
+ args_cli.seed = config.seed
224
+ args_cli.env_spacing = env_config.config.env_spacing
225
+ args_cli.output_dir = config.output_dir
226
+ args_cli.enable_cameras = env_config.config.get(
227
+ "render_results", False
228
+ ) or env_config.config.get("enable_cameras", False)
229
+
230
+ args_cli.headless = config.headless
231
+ args_cli.multi_gpu = config.multi_gpu
232
+ args_cli.distributed = config.multi_gpu
233
+ args_cli.device = device
234
+
235
+ base_kit_args = (
236
+ "--/log/level=error --/log/fileLogLevel=error --/log/outputStreamLevel=error"
237
+ )
238
+ if args_cli.headless:
239
+ args_cli.kit_args = base_kit_args + " --no-window"
240
+ else:
241
+ args_cli.kit_args = base_kit_args + f" --/renderer/activeGpu={render_gpu_idx}"
242
+
243
+ # Allow air-gapped machines to use an experience file with online
244
+ # extension registries disabled, while preserving the default behavior.
245
+ offline_experience = os.environ.get("ISAACLAB_EXPERIENCE")
246
+ if offline_experience:
247
+ args_cli.experience = offline_experience
248
+
249
+ _lock_path = "/tmp/isaaclab_app_launcher.lock" # noqa: S108
250
+ with filelock.FileLock(_lock_path):
251
+ app_launcher = AppLauncher(args_cli)
252
+ simulation_app = app_launcher.app # noqa: F841
253
+
254
+ import torch
255
+
256
+ torch.backends.cuda.matmul.allow_tf32 = True
257
+ torch.backends.cudnn.allow_tf32 = True
258
+ torch.backends.cudnn.deterministic = False
259
+ torch.backends.cudnn.benchmark = False
260
+
261
+ unresolved_conf = omegaconf.OmegaConf.to_container(config, resolve=False) # noqa: F841
262
+ os.chdir(hydra.utils.get_original_cwd())
263
+
264
+ ckpt_num = config.checkpoint.split("/")[-1].split("_")[-1].split(".")[0]
265
+
266
+ if env_config.config.get("save_rendering_dir", None) is None:
267
+ env_config.config.save_rendering_dir = str(
268
+ checkpoint.parent / "renderings" / f"ckpt_{ckpt_num}"
269
+ )
270
+
271
+ metrics_file = config.get("metrics_file", None)
272
+ if metrics_file is not None:
273
+ metrics_file = Path(metrics_file)
274
+ assert metrics_file.exists(), f"Metrics file {metrics_file} does not exist"
275
+ if metrics_file.exists():
276
+ metrics = json.load(open(metrics_file)) # noqa: SIM115
277
+ all_dict = metrics["eval/all_metrics_dict"]
278
+
279
+ # Check if this is grab evaluation (has success_lift)
280
+ has_obj_metrics = "obj_pos_error" in all_dict
281
+ if "success_lift" in all_dict:
282
+ # Grab evaluation: prioritize failed grasps (not lifted) and terminated trajectories
283
+ motion_keys = all_dict["motion_keys"]
284
+ terminated = all_dict["terminated"]
285
+ success_lift = all_dict["success_lift"]
286
+ progress = all_dict.get("progress", [1.0] * len(motion_keys))
287
+ obj_pos_errors = all_dict.get("obj_pos_error", [0.0] * len(motion_keys))
288
+
289
+ pairs = []
290
+ for i in range(len(motion_keys)):
291
+ term = bool(terminated[i]) if i < len(terminated) else False
292
+ lifted = bool(success_lift[i]) if i < len(success_lift) else False
293
+ prog = progress[i] if i < len(progress) else 1.0
294
+ obj_err = obj_pos_errors[i] if i < len(obj_pos_errors) else 0.0
295
+ priority = 0 if not lifted else (1 if term else 2)
296
+ pairs.append((motion_keys[i], term, lifted, prog, obj_err, priority))
297
+
298
+ pairs_sorted = sorted(pairs, key=lambda x: (x[5], x[3]))
299
+ if len(pairs_sorted) > config.num_envs:
300
+ pairs_sorted = pairs_sorted[: config.num_envs]
301
+
302
+ render_info = []
303
+ for pair in pairs_sorted:
304
+ motion_key, term, lifted, prog, obj_err, _ = pair
305
+ status = "FAILED" if not lifted else ("TERMINATED" if term else "SUCCESS")
306
+ info = [
307
+ f"{motion_key}",
308
+ f"lifted: {lifted}",
309
+ f"progress: {prog:.3f}",
310
+ f"status: {status}",
311
+ ]
312
+ if has_obj_metrics:
313
+ info.append(f"obj_pos_err: {obj_err:.4f}m")
314
+ render_info.append(tuple(info))
315
+
316
+ filter_keys = [pair[0] for pair in pairs_sorted]
317
+
318
+ with omegaconf.open_dict(env_config.config):
319
+ env_config.config.render_info = render_info
320
+ env_config.config.max_render_envs = len(render_info)
321
+ with omegaconf.open_dict(env_config.commands.motion):
322
+ env_config.commands.motion.filter_motion_keys = filter_keys
323
+ if "motion_lib_cfg" in env_config.commands.motion:
324
+ env_config.commands.motion.motion_lib_cfg.filter_motion_keys = filter_keys
325
+ else:
326
+ # Imitation evaluation: use MPJPE-based sorting
327
+ obj_pos_errors = all_dict.get("obj_pos_error", None)
328
+ success_pair = [
329
+ (
330
+ all_dict["motion_keys"][i],
331
+ all_dict["mpjpe_l"][i],
332
+ all_dict["mpjpe_g"][i],
333
+ True,
334
+ obj_pos_errors[i] if obj_pos_errors is not None else 0.0,
335
+ )
336
+ for i in range(len(all_dict["motion_keys"]))
337
+ if not all_dict["terminated"][i]
338
+ ]
339
+ render_sort_by = config.get("render_sort_by", "mpjpe_l")
340
+ sort_idx = 4 if render_sort_by == "obj_pos_error" else 1
341
+ success_pair_sorted = sorted(success_pair, key=lambda x: x[sort_idx], reverse=True)
342
+ failed_pair = [
343
+ (
344
+ all_dict["motion_keys"][i],
345
+ all_dict["mpjpe_l"][i],
346
+ all_dict["mpjpe_g"][i],
347
+ False,
348
+ obj_pos_errors[i] if obj_pos_errors is not None else 0.0,
349
+ )
350
+ for i in range(len(all_dict["motion_keys"]))
351
+ if all_dict["terminated"][i]
352
+ ]
353
+ failed_pair_sorted = sorted(failed_pair, key=lambda x: x[sort_idx], reverse=True)
354
+ all_pair = failed_pair_sorted + success_pair_sorted
355
+ if len(all_pair) > config.num_envs:
356
+ all_pair = all_pair[: config.num_envs]
357
+ render_info = []
358
+ for pair in all_pair:
359
+ info = [
360
+ f"{pair[0]}",
361
+ f"mpjpe_l: {pair[1]:.2f}",
362
+ f"mpjpe_g: {pair[2]:.2f}",
363
+ f"success: {pair[3]}",
364
+ ]
365
+ if has_obj_metrics:
366
+ info.append(f"obj_pos_err: {pair[4]:.4f}m")
367
+ render_info.append(tuple(info))
368
+ with omegaconf.open_dict(env_config.config):
369
+ env_config.config.render_info = render_info
370
+ env_config.config.max_render_envs = len(all_pair)
371
+ filter_keys = [pair[0] for pair in all_pair]
372
+ with omegaconf.open_dict(env_config.commands.motion):
373
+ env_config.commands.motion.filter_motion_keys = filter_keys
374
+ if "motion_lib_cfg" in env_config.commands.motion:
375
+ env_config.commands.motion.motion_lib_cfg.filter_motion_keys = filter_keys
376
+
377
+ env = train_agent_trl.create_manager_env(config, device, args_cli)
378
+
379
+ module_dim_dict = getattr(config.algo.config, "module_dim", {})
380
+ policy_backbone_kwargs = {}
381
+ critic_backbone_kwargs = {}
382
+ env.config["obs"]["obs_dims"]["actor_obs"] = env.env.observation_space["policy"].shape[-1]
383
+ env.config["obs"]["obs_dims"]["critic_obs"] = env.env.observation_space["critic"].shape[-1]
384
+ env.config["robot"]["algo_obs_dim_dict"]["actor_obs"] = env.env.observation_space[
385
+ "policy"
386
+ ].shape[-1]
387
+ env.config["robot"]["algo_obs_dim_dict"]["critic_obs"] = env.env.observation_space[
388
+ "critic"
389
+ ].shape[-1]
390
+ example_obs = env.reset(flatten_dict_obs=False)
391
+ for key in env.env.observation_space:
392
+ if key not in ["policy", "critic"]:
393
+ group_obs_dims, group_obs_names, group_obs_total_dim = (
394
+ obs_utils.get_group_term_obs_shape(example_obs, key)
395
+ )
396
+ env.config["obs"]["group_obs_dims"][key] = group_obs_dims
397
+ env.config["obs"]["group_obs_names"][key] = group_obs_names
398
+ env.config["obs"]["obs_dims"][key] = group_obs_total_dim
399
+ env.config["robot"]["algo_obs_dim_dict"][key] = group_obs_total_dim
400
+
401
+ meta_action_dim = env.config.get("meta_action_dim", None)
402
+ if meta_action_dim is not None and meta_action_dim > 0:
403
+ env.config["robot"]["actions_dim"] = meta_action_dim
404
+ else:
405
+ env.config["robot"]["actions_dim"] = env.env.action_space.shape[-1]
406
+
407
+ policy = trl_utils_common.custom_instantiate(
408
+ config.algo.config.actor,
409
+ env_config=env.config,
410
+ algo_config=config.algo.config,
411
+ module_dim_dict=module_dim_dict,
412
+ backbone_kwargs=policy_backbone_kwargs,
413
+ _resolve=False,
414
+ ).to(device)
415
+
416
+ if not getattr(config.algo.config, "distill_only", False):
417
+ value_model = trl_utils_common.custom_instantiate(
418
+ config.algo.config.critic,
419
+ env_config=env.config,
420
+ algo_config=config.algo.config,
421
+ module_dim_dict=module_dim_dict,
422
+ backbone_kwargs=critic_backbone_kwargs,
423
+ _resolve=False,
424
+ ).to(device)
425
+
426
+ accelerator.wait_for_everyone()
427
+
428
+ args = easydict.EasyDict()
429
+ args.is_main_process = accelerator.is_main_process
430
+ args.global_rank = accelerator.process_index
431
+ args.world_size = accelerator.num_processes
432
+ state = easydict.EasyDict()
433
+
434
+ from gear_sonic.trl.trainer import ppo_trainer
435
+
436
+ model = ppo_trainer.PolicyAndValueWrapper(policy, value_model)
437
+
438
+ checkpoint_path = str(config.checkpoint)
439
+ logger.info(f"Loading checkpoint from {checkpoint_path}")
440
+ checkpoint = torch.load(checkpoint_path, map_location=accelerator.device, weights_only=False)
441
+
442
+ # Load policy state dict with backward compatibility for std/log_std
443
+ if "actor_model_state_dict" in checkpoint:
444
+ state_dict = checkpoint["actor_model_state_dict"]
445
+ elif "policy_state_dict" in checkpoint:
446
+ state_dict = checkpoint["policy_state_dict"]
447
+ else:
448
+ state_dict = None
449
+
450
+ if state_dict is not None:
451
+ model_uses_std = "std" in model.policy.state_dict()
452
+ checkpoint_has_std = "std" in state_dict
453
+ checkpoint_has_log_std = "log_std" in state_dict
454
+
455
+ logger.info(f"Model parameterization: {'std' if model_uses_std else 'log_std'}")
456
+ logger.info(
457
+ f"Checkpoint parameterization: {'std' if checkpoint_has_std else 'log_std' if checkpoint_has_log_std else 'unknown'}" # noqa: E501
458
+ )
459
+
460
+ if model_uses_std and checkpoint_has_log_std and not checkpoint_has_std:
461
+ logger.info("Transforming 'log_std' -> 'std' (applying exp) for backward compatibility")
462
+ state_dict["std"] = torch.exp(state_dict.pop("log_std"))
463
+ elif not model_uses_std and checkpoint_has_std and not checkpoint_has_log_std:
464
+ logger.info("Transforming 'std' -> 'log_std' (applying log) for backward compatibility")
465
+ state_dict["log_std"] = torch.log(state_dict.pop("std"))
466
+
467
+ model.policy.load_state_dict(state_dict)
468
+ logger.info("Successfully loaded policy state dict")
469
+
470
+ state.global_step = checkpoint["state"].global_step
471
+
472
+ schedule_wrapper = easydict.EasyDict(env=env, model=model)
473
+ if "schedule_dict" in config.trainer:
474
+ scheduled_params_dict = scheduler.update_scheduled_params( # noqa: F841
475
+ schedule_wrapper, config.trainer.schedule_dict, state.global_step
476
+ )
477
+ env.reinit_dr()
478
+
479
+ global_step = checkpoint["state"].global_step
480
+ exported_policy_path = os.path.join(config.experiment_dir, "exported")
481
+ os.makedirs(exported_policy_path, exist_ok=True)
482
+ exported_onnx_name = f"model_step_{global_step:06d}.onnx"
483
+ new_cp_path = f"{os.path.dirname(config.checkpoint)}/model_step_{global_step:06d}.pt"
484
+ if not os.path.exists(new_cp_path):
485
+ shutil.copy(checkpoint_path, new_cp_path)
486
+
487
+ if config.get("export_onnx_only", False):
488
+
489
+ def get_example_obs():
490
+ obs_dict = env.reset_all()
491
+ for k in obs_dict:
492
+ obs_dict[k] = obs_dict[k].cpu()
493
+ return obs_dict
494
+
495
+ assert config.num_envs == 1, "num_envs must be 1 for exporting onnx"
496
+ from gear_sonic.utils import inference_helpers
497
+
498
+ example_obs_dict = get_example_obs()
499
+
500
+ # Check if actor has universal-token encoder structure
501
+ has_actor_module = hasattr(model.policy, "actor_module")
502
+ has_encoders = has_actor_module and hasattr(
503
+ model.policy.actor_module, "encoders_to_iterate"
504
+ )
505
+
506
+ if "tokenizer" in example_obs_dict and has_encoders:
507
+
508
+ inference_helpers.export_universal_token_module_as_onnx(
509
+ model.policy.actor_module,
510
+ encoder_name="smpl",
511
+ decoder_name="g1_dyn",
512
+ path=exported_policy_path,
513
+ exported_model_name=exported_onnx_name.replace(".onnx", "_smpl.onnx"),
514
+ batch_size=1,
515
+ )
516
+ inference_helpers.export_universal_token_module_as_onnx(
517
+ model.policy.actor_module,
518
+ encoder_name="g1",
519
+ decoder_name="g1_dyn",
520
+ path=exported_policy_path,
521
+ exported_model_name=exported_onnx_name.replace(".onnx", "_g1.onnx"),
522
+ batch_size=1,
523
+ )
524
+ inference_helpers.export_universal_token_module_as_onnx(
525
+ model.policy.actor_module,
526
+ encoder_name="teleop",
527
+ decoder_name="g1_dyn",
528
+ path=exported_policy_path,
529
+ exported_model_name=exported_onnx_name.replace(".onnx", "_teleop.onnx"),
530
+ batch_size=1,
531
+ )
532
+
533
+ inference_helpers.export_universal_token_encoders_as_onnx(
534
+ model.policy.actor_module,
535
+ path=exported_policy_path,
536
+ exported_model_name=exported_onnx_name.replace(".onnx", "_encoder.onnx"),
537
+ batch_size=1,
538
+ )
539
+ inference_helpers.export_universal_token_decoder_as_onnx(
540
+ model.policy.actor_module,
541
+ decoder_name="g1_dyn",
542
+ path=exported_policy_path,
543
+ exported_model_name=exported_onnx_name.replace(".onnx", "_decoder.onnx"),
544
+ batch_size=1,
545
+ )
546
+ print( # noqa: T201
547
+ f'Exported encoders ONNX to {os.path.join(exported_policy_path, exported_onnx_name.replace(".onnx", "_encoder.onnx"))}' # noqa: E501
548
+ )
549
+ print( # noqa: T201
550
+ f'Exported decoder ONNX to {os.path.join(exported_policy_path, exported_onnx_name.replace(".onnx", "_decoder.onnx"))}' # noqa: E501
551
+ )
552
+
553
+ else:
554
+ inference_helpers.export_policy_as_onnx(
555
+ {"actor": model.policy}, exported_policy_path, exported_onnx_name, example_obs_dict
556
+ )
557
+
558
+ logger.info(f"Exported policy as onnx to: {os.path.join(exported_policy_path)}")
559
+
560
+ # Export configs to YAML
561
+ export_config = {
562
+ "env_config": omegaconf.OmegaConf.to_container(env.config, resolve=True),
563
+ "algo_config": omegaconf.OmegaConf.to_container(config.algo.config, resolve=True),
564
+ }
565
+ config_yaml_path = os.path.join(os.path.dirname(config.checkpoint), "model_config.yaml")
566
+ with open(config_yaml_path, "w") as f:
567
+ yaml.dump(export_config, f, default_flow_style=False)
568
+ logger.info(f"Exported config to: {config_yaml_path}")
569
+ exit() # noqa: PLR1722
570
+
571
+ eval_callbacks = config.get("eval_callbacks", [])
572
+ if isinstance(eval_callbacks, str):
573
+ eval_callbacks = [eval_callbacks]
574
+
575
+ callbacks = {}
576
+ for callback_name in eval_callbacks:
577
+ if callback_name == "im_eval":
578
+ with omegaconf.open_dict(config.callbacks.im_eval):
579
+ config.callbacks.im_eval.eval_only = True
580
+ config.callbacks.im_eval.eval_frequency = 1
581
+ config.callbacks.im_eval.output_dir = config.get("eval_output_dir", None)
582
+ config.callbacks.im_eval.log_keys = config.get("log_keys", None)
583
+ if callback_name not in config.callbacks:
584
+ raise ValueError(f"Callback {callback_name} not found")
585
+ callbacks[callback_name] = utils.instantiate(config.callbacks[callback_name])
586
+
587
+ for callback_name, callback in callbacks.items(): # noqa: B007
588
+ if hasattr(callback, "model") and callback.model is None:
589
+ callback.model = model
590
+
591
+ for callback_name, callback in callbacks.items(): # noqa: B007
592
+ callback.on_step_end(args, state, None, env=env, model=model, accelerator=accelerator)
593
+
594
+ if config.get("run_eval_loop", True):
595
+ env.set_is_evaluating(True)
596
+ obs_dict = env.reset_all()
597
+ model.eval()
598
+ for obs_key in obs_dict:
599
+ obs_dict[obs_key] = obs_dict[obs_key].to(device)
600
+
601
+ eval_step_callbacks = {
602
+ name: cb
603
+ for name, cb in callbacks.items()
604
+ if hasattr(cb, "eval_step") and callable(getattr(cb, "eval_step")) # noqa: B009
605
+ }
606
+ if eval_step_callbacks:
607
+ logger.info(f"Eval step callbacks enabled: {list(eval_step_callbacks.keys())}")
608
+
609
+ step_count = 0
610
+ max_render_steps = config.get("max_render_steps", 0)
611
+
612
+ run_once = config.get("run_once", False)
613
+ envs_completed = torch.zeros(config.num_envs, dtype=torch.bool, device=device)
614
+
615
+ with torch.no_grad():
616
+ while True:
617
+ policy_model = model.policy
618
+ value_model = model.value_model
619
+ policy_model.init_rollout()
620
+
621
+ actor_state = {}
622
+ actions = policy_model.rollout(obs_dict=obs_dict)
623
+ actor_state["actions"] = policy_model.action_mean.detach()
624
+ actor_state["obs_dict"] = actions["obs_dict"]
625
+
626
+ step_count += 1
627
+
628
+ if max_render_steps > 0 and step_count >= max_render_steps:
629
+ logger.info(f"Reached max_render_steps={max_render_steps}. Exiting.")
630
+ if hasattr(env, "end_render_results"):
631
+ env.end_render_results()
632
+ break
633
+
634
+ results = env.step(actor_state)
635
+ obs_dict, rewards, dones, infos = (
636
+ results[0],
637
+ results[1],
638
+ results[2],
639
+ results[3],
640
+ ) # noqa: F841
641
+
642
+ if eval_step_callbacks:
643
+ all_want_exit = all(
644
+ cb.eval_step(env, results) for cb in eval_step_callbacks.values()
645
+ )
646
+ if all_want_exit:
647
+ logger.info("All eval step callbacks signaled exit. Exiting evaluation loop.")
648
+ break
649
+
650
+ if run_once:
651
+ envs_completed = (
652
+ envs_completed | dones.squeeze(-1)
653
+ if dones.dim() > 1
654
+ else envs_completed | dones
655
+ )
656
+ if envs_completed.all():
657
+ logger.info("All environments completed one episode. Exiting (run_once=True).")
658
+ if hasattr(env, "end_render_results"):
659
+ env.end_render_results()
660
+ break
661
+
662
+ for obs_key in obs_dict.keys(): # noqa: SIM118
663
+ obs_dict[obs_key] = obs_dict[obs_key].to(device)
664
+
665
+ if simulator_type == "IsaacSim":
666
+ os._exit(0)
667
+
668
+
669
+ if __name__ == "__main__":
670
+ main()
GR00T-WholeBodyControl/gear_sonic/eval_exp.py ADDED
@@ -0,0 +1,679 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3 # noqa: EXE001
2
+ # Copyright 2025 The HuggingFace Team. All rights reserved.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ import glob
17
+ import itertools
18
+ import json
19
+ import os
20
+ from pathlib import Path
21
+ import signal
22
+ import subprocess
23
+ import sys
24
+ import time
25
+ import hydra
26
+ from loguru import logger
27
+ import omegaconf
28
+ import wandb
29
+ import yaml
30
+
31
+ from gear_sonic.trl.callbacks import im_eval_callback
32
+ from gear_sonic.utils import config_utils
33
+
34
+ config_utils.register_rl_resolvers()
35
+
36
+
37
+ class CheckpointEvaluator:
38
+ """Continuously monitors an experiment directory for new checkpoints and evaluates them sequentially."""
39
+
40
+ def __init__(self, config):
41
+ self.config = config
42
+ self.experiment_dir = Path(config.experiment_dir)
43
+ self.evaluated_checkpoints: set[str] = set()
44
+ self.shutdown_flag = False
45
+ self.last_evaluation_time = time.time()
46
+ self.evaluation_timeout = config.get("evaluation_timeout", 24 * 3600)
47
+ self.eval_frequency = config.get("eval_frequency", None)
48
+ self.eval_last_n = config.get("eval_last_n", None)
49
+
50
+ if not self.experiment_dir.exists():
51
+ raise ValueError(f"Experiment directory does not exist: {self.experiment_dir}")
52
+
53
+ self.find_evaluated_checkpoints()
54
+
55
+ self.wandb_run_id = None
56
+ self.wandb_project = None
57
+ self.wandb_entity = None
58
+ self._load_wandb_config()
59
+
60
+ logger.info(f"Monitoring experiment directory: {self.experiment_dir}")
61
+ logger.info(f"Scan interval: {config.scan_interval} seconds")
62
+ logger.info(f"Evaluation timeout: {self.evaluation_timeout / 3600:.1f} hours")
63
+ if self.wandb_run_id:
64
+ logger.info(f"Wandb logging enabled: run_id={self.wandb_run_id}")
65
+
66
+ self._backfill_wandb()
67
+
68
+ signal.signal(signal.SIGINT, self._signal_handler)
69
+ signal.signal(signal.SIGTERM, self._signal_handler)
70
+
71
+ def _signal_handler(self, signum, frame): # noqa: ARG002
72
+ logger.info("Received shutdown signal. Stopping checkpoint monitoring...")
73
+ self.shutdown_flag = True
74
+
75
+ def _load_wandb_config(self):
76
+ meta_path = self.experiment_dir / "meta.yaml"
77
+ if meta_path.exists():
78
+ with open(meta_path) as f:
79
+ meta = yaml.safe_load(f)
80
+ self.wandb_run_id = meta.get("wandb_run")
81
+
82
+ config_path = self.experiment_dir / ".hydra" / "config.yaml"
83
+ if not config_path.exists():
84
+ config_path = self.experiment_dir / "config.yaml"
85
+ if config_path.exists():
86
+ try:
87
+ with open(config_path) as f:
88
+ train_config = yaml.safe_load(f)
89
+ wandb_cfg = train_config.get("wandb", {})
90
+ self.wandb_project = train_config.get("project_name", "TRL_G1_Track")
91
+ self.wandb_entity = wandb_cfg.get("wandb_entity", None)
92
+ except Exception as e: # noqa: BLE001
93
+ logger.warning(f"Could not load training config for wandb: {e}")
94
+
95
+ if self.wandb_project is None:
96
+ self.wandb_project = "TRL_G1_Track"
97
+ if self.wandb_entity is None:
98
+ self.wandb_entity = None # uses wandb default entity
99
+
100
+ def _get_wandb_logged_steps(self) -> set[int]:
101
+ logged_steps = set()
102
+ if not self.wandb_run_id:
103
+ return logged_steps
104
+
105
+ try:
106
+ api = wandb.Api(timeout=30)
107
+ run = api.run(f"{self.wandb_entity}/{self.wandb_project}/{self.wandb_run_id}")
108
+ hist = run.scan_history(
109
+ keys=["eval/success/success_rate", "eval_step"],
110
+ min_step=0,
111
+ page_size=10000,
112
+ )
113
+ for row in hist:
114
+ if row.get("eval/success/success_rate") is not None:
115
+ step = row.get("eval_step")
116
+ if step is not None:
117
+ logged_steps.add(int(step))
118
+ except Exception as e: # noqa: BLE001
119
+ logger.warning(f"Could not query wandb for logged eval steps: {e}")
120
+
121
+ return logged_steps
122
+
123
+ def _backfill_wandb(self):
124
+ if not self.wandb_run_id:
125
+ self._load_wandb_config()
126
+ if not self.wandb_run_id:
127
+ logger.info("No wandb run ID available, skipping backfill")
128
+ return
129
+
130
+ eval_dir = self.experiment_dir / "eval"
131
+ if not eval_dir.exists():
132
+ return
133
+
134
+ completed_steps = []
135
+ for eval_subdir in sorted(eval_dir.iterdir()):
136
+ if not eval_subdir.is_dir():
137
+ continue
138
+ try:
139
+ step_num = int(eval_subdir.name)
140
+ except ValueError:
141
+ continue
142
+ if (eval_subdir / "all_eval_finish.txt").exists():
143
+ completed_steps.append((step_num, str(eval_subdir)))
144
+
145
+ if not completed_steps:
146
+ logger.info("No completed eval steps found on disk, nothing to backfill")
147
+ return
148
+
149
+ logged_steps = self._get_wandb_logged_steps()
150
+ missing = [(step, path) for step, path in completed_steps if step not in logged_steps]
151
+
152
+ if not missing:
153
+ logger.info(
154
+ f"All {len(completed_steps)} eval steps already logged to wandb, no backfill needed"
155
+ )
156
+ return
157
+
158
+ logger.info(f"Backfilling {len(missing)}/{len(completed_steps)} eval steps to wandb")
159
+ for eval_step, checkpoint_work_dir in missing:
160
+ self._log_eval_to_wandb(eval_step, checkpoint_work_dir)
161
+
162
+ logger.info("Backfill complete")
163
+
164
+ def _log_eval_to_wandb(self, eval_step: int, checkpoint_work_dir: str):
165
+ if not self.wandb_run_id:
166
+ self._load_wandb_config()
167
+ if not self.wandb_run_id:
168
+ logger.warning("No wandb run ID found, skipping wandb logging")
169
+ return
170
+
171
+ eval_dir = Path(checkpoint_work_dir)
172
+ if not eval_dir.exists():
173
+ return
174
+
175
+ try:
176
+ wandb.init(
177
+ id=self.wandb_run_id,
178
+ project=self.wandb_project,
179
+ entity=self.wandb_entity,
180
+ resume="allow",
181
+ )
182
+
183
+ wandb.define_metric("eval_step")
184
+ wandb.define_metric("eval/*", step_metric="eval_step")
185
+ wandb.define_metric("videos_hard*", step_metric="eval_step")
186
+ for subdir in sorted(eval_dir.iterdir()):
187
+ if subdir.is_dir() and subdir.name != "train":
188
+ wandb.define_metric(f"{subdir.name}/*", step_metric="eval_step")
189
+
190
+ all_metrics = {"eval_step": eval_step}
191
+
192
+ for subdir in sorted(eval_dir.iterdir()):
193
+ if not subdir.is_dir():
194
+ continue
195
+
196
+ try:
197
+ metrics_file = subdir / "metrics_eval.json"
198
+ metrics_finish = subdir / "metrics_finish.txt"
199
+ if metrics_finish.exists() and metrics_file.exists():
200
+ self._log_metrics(eval_step, metrics_file)
201
+
202
+ render_finish = subdir / "render_finish.txt"
203
+ video_dir = subdir / "render_results"
204
+ if render_finish.exists() and video_dir.exists():
205
+ self._log_videos(eval_step, metrics_file, video_dir)
206
+ except Exception as subdir_e: # noqa: BLE001
207
+ logger.error(
208
+ f"Failed to log subdir {subdir.name} for step {eval_step}: {subdir_e}"
209
+ )
210
+
211
+ wandb.log(all_metrics)
212
+ wandb.finish()
213
+ logger.info(f"Logged eval results to wandb for step {eval_step}")
214
+
215
+ except Exception as e: # noqa: BLE001
216
+ logger.error(f"Failed to log to wandb for step {eval_step}: {e}")
217
+ try:
218
+ wandb.finish()
219
+ except Exception as e: # noqa: BLE001
220
+ logger.error(f"Error finishing wandb: {e}")
221
+
222
+ def _load_metrics(self, eval_step: int, metrics_file: Path) -> dict | None:
223
+ try:
224
+ with open(metrics_file) as f:
225
+ metrics_eval = json.load(f)
226
+ except json.JSONDecodeError:
227
+ logger.error(f"Error loading {metrics_file}")
228
+ return None
229
+
230
+ log_keys = metrics_eval.pop("log_keys", None)
231
+
232
+ file_size_mb = metrics_file.stat().st_size / 1024 / 1024
233
+ if file_size_mb > 20:
234
+ metrics_eval.pop("eval/all_metrics_dict", None)
235
+ metrics_eval.pop("eval/failed_metrics_dict", None)
236
+ logger.info(
237
+ f"Skipping per-motion dicts for {metrics_file.parent.name} ({file_size_mb:.0f} MB > 20 MB)"
238
+ )
239
+ else:
240
+ if "eval/all_metrics_dict" in metrics_eval:
241
+ metrics_eval["eval/all_metrics_dict"] = im_eval_callback.create_html_table(
242
+ metrics_eval["eval/all_metrics_dict"]
243
+ )
244
+ if "eval/failed_metrics_dict" in metrics_eval:
245
+ metrics_eval["eval/failed_metrics_dict"] = im_eval_callback.create_html_table(
246
+ metrics_eval["eval/failed_metrics_dict"]
247
+ )
248
+
249
+ for key in ["failed_keys", "failed_idxes"]:
250
+ metrics_eval.pop(key, None)
251
+
252
+ metrics_eval["eval_step"] = eval_step
253
+
254
+ if log_keys is not None:
255
+ metrics_eval = {f"{log_keys}/{k}": v for k, v in metrics_eval.items()}
256
+ metrics_eval["eval_step"] = eval_step
257
+
258
+ return metrics_eval
259
+
260
+ def _log_metrics(self, eval_step: int, metrics_file: Path):
261
+ metrics = self._load_metrics(eval_step, metrics_file)
262
+ if metrics:
263
+ wandb.log(metrics)
264
+
265
+ def _log_videos(self, eval_step: int, metrics_file: Path, video_dir: Path):
266
+ if not video_dir.exists():
267
+ return
268
+
269
+ log_keys = None
270
+ if metrics_file.exists():
271
+ try:
272
+ with open(metrics_file) as f:
273
+ metrics = json.load(f)
274
+ log_keys = metrics.get("log_keys")
275
+ except Exception as e: # noqa: BLE001
276
+ logger.error(f"Error getting log_keys from metrics file: {e}")
277
+
278
+ video_files = sorted(
279
+ [
280
+ (i, f)
281
+ for i, f in enumerate(sorted(video_dir.iterdir()))
282
+ if f.is_file() and f.name.endswith(".mp4")
283
+ ]
284
+ )
285
+
286
+ if not video_files:
287
+ return
288
+
289
+ prefix = f"videos_hard_{log_keys}" if log_keys else "videos_hard"
290
+ wandb_videos = {
291
+ f"{prefix}/{i:04d}": wandb.Video(str(video_file), format="mp4")
292
+ for i, video_file in reversed(video_files)
293
+ }
294
+ wandb_videos["eval_step"] = eval_step
295
+ wandb.log(wandb_videos)
296
+
297
+ def find_evaluated_checkpoints(self):
298
+ """Find all checkpoints that have been successfully evaluated."""
299
+ eval_dir = self.experiment_dir / "eval"
300
+
301
+ if not eval_dir.exists():
302
+ logger.info("No eval directory found")
303
+ return
304
+
305
+ for eval_subdir in sorted(eval_dir.iterdir()):
306
+ if eval_subdir.is_dir():
307
+ metrics_finish_file = eval_subdir / "metrics_finish.txt"
308
+ metrics_file = eval_subdir / "metrics_eval.json"
309
+ render_finish_file = eval_subdir / "render_finish.txt"
310
+ if (
311
+ metrics_finish_file.exists()
312
+ and metrics_file.exists()
313
+ and render_finish_file.exists()
314
+ ):
315
+ try:
316
+ step_num = int(eval_subdir.name)
317
+ checkpoint_path = (
318
+ self.experiment_dir / f"model_step_{step_num:06d}.pt"
319
+ )
320
+ if checkpoint_path.exists():
321
+ self.evaluated_checkpoints.add(str(checkpoint_path))
322
+ except ValueError:
323
+ pass
324
+
325
+ logger.info(f"Found {len(self.evaluated_checkpoints)} already evaluated checkpoints")
326
+
327
+ def find_checkpoints(self) -> list[Path]:
328
+ """Find all checkpoint files in the experiment directory."""
329
+ checkpoint_pattern = str(self.experiment_dir / "model_step_*.pt")
330
+ checkpoints = sorted(
331
+ [Path(p) for p in glob.glob(checkpoint_pattern)],
332
+ key=lambda p: int(p.stem.split("_")[-1]),
333
+ )
334
+ return checkpoints
335
+
336
+ def is_checkpoint_ready(self, checkpoint_path: Path) -> bool:
337
+ """Check if a checkpoint is ready for evaluation (not being written)."""
338
+ checkpoint_ready_delay = self.config.get("checkpoint_ready_delay", 60)
339
+ mtime = checkpoint_path.stat().st_mtime
340
+ age = time.time() - mtime
341
+ return age > checkpoint_ready_delay
342
+
343
+ def evaluate_checkpoint(
344
+ self,
345
+ checkpoint_path: Path,
346
+ mode: str = "metrics",
347
+ work_dir: str = None,
348
+ num_render_videos: int = None,
349
+ eval_step: int = None, # noqa: ARG002
350
+ eval_dataset: str = None,
351
+ eval_mode: str = None,
352
+ ):
353
+ """Evaluate a single checkpoint using eval_agent_trl.py."""
354
+ checkpoint_str = str(checkpoint_path)
355
+ success = False
356
+
357
+ mode_finish_file = os.path.join(work_dir, f"{mode}_finish.txt")
358
+ metrics_file = os.path.join(work_dir, "metrics_eval.json")
359
+ skip = os.path.exists(mode_finish_file)
360
+ if skip and mode == "metrics" and not os.path.exists(metrics_file):
361
+ logger.info(f"[{mode}] Not skipping since metrics file not found: {metrics_file}")
362
+ skip = False
363
+ if skip:
364
+ logger.info(
365
+ f"[{mode}] Skipping evaluation for checkpoint: {checkpoint_path} because it has already been evaluated" # noqa: E501
366
+ )
367
+ return True
368
+
369
+ try:
370
+ logger.info(f"[{mode}] Starting evaluation for checkpoint: {checkpoint_path}")
371
+
372
+ eval_callbacks = self.config.get("eval_callbacks", "im_eval")
373
+
374
+ if mode == "metrics":
375
+ cmd = f"accelerate launch gear_sonic/eval_agent_trl.py +checkpoint={checkpoint_str} +headless=True ++eval_callbacks={eval_callbacks} ++run_eval_loop=False" # noqa: E501
376
+ cmd += f" ++num_envs={self.config.num_eval_envs}"
377
+ cmd += f" ++eval_output_dir={work_dir}"
378
+ if eval_mode is not None:
379
+ cmd += f" ++use_encoder={eval_mode}"
380
+ cmd += " ++manager_env.commands.motion.motion_lib_cfg.multi_thread=False"
381
+ cmd += " +manager_env/terminations=tracking/eval"
382
+ if eval_dataset is not None:
383
+ cmd += (
384
+ f" +manager_env.commands.motion.motion_lib_cfg.motion_file={eval_dataset}"
385
+ )
386
+ cmd += f" +log_keys={Path(eval_dataset).name}_{eval_mode if eval_mode is not None else 'all'}"
387
+
388
+ elif mode == "render":
389
+ cmd = f"python -u gear_sonic/eval_agent_trl.py +checkpoint={checkpoint_str} +headless=True ++eval_callbacks={eval_callbacks} ++run_eval_loop=False" # noqa: E501
390
+ cmd += f" ++num_envs={num_render_videos}"
391
+ cmd += f" ++metrics_file={metrics_file}"
392
+ render_sort_by = self.config.get("render_sort_by", None)
393
+ if render_sort_by is not None:
394
+ cmd += f" ++render_sort_by={render_sort_by}"
395
+ cmd += f" ++manager_env.config.save_rendering_dir={work_dir}/render_results"
396
+ cmd += " ++manager_env.config.render_results=True"
397
+ cmd += " ++manager_env.config.env_spacing=10.0"
398
+ cmd += " +manager_env/recorders=render"
399
+ cmd += " ++manager_env.commands.motion.motion_lib_cfg.multi_thread=False"
400
+ if eval_mode is not None:
401
+ cmd += f" ++use_encoder={eval_mode}"
402
+
403
+ if eval_dataset is not None:
404
+ cmd += (
405
+ f" +manager_env.commands.motion.motion_lib_cfg.motion_file={eval_dataset}"
406
+ )
407
+
408
+ extra_overrides = self.config.get("extra_overrides", [])
409
+ for override in extra_overrides:
410
+ cmd += f" {override}"
411
+
412
+ logger.info(f"Running command: {cmd}")
413
+ capture_output = self.config.get("capture_output", True)
414
+ timeout_seconds = self.config.get("render_timeout", 3600) if mode == "render" else 21600
415
+ proc = subprocess.Popen(
416
+ cmd,
417
+ shell=True,
418
+ preexec_fn=os.setsid,
419
+ stdout=subprocess.PIPE if capture_output else None,
420
+ stderr=subprocess.PIPE if capture_output else None,
421
+ text=True,
422
+ )
423
+ try:
424
+ stdout_data, stderr_data = proc.communicate(timeout=timeout_seconds)
425
+ except subprocess.TimeoutExpired:
426
+ os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
427
+ proc.communicate()
428
+ logger.error(f"Evaluation timeout for checkpoint: {checkpoint_path}")
429
+ return False
430
+ result_returncode = proc.returncode
431
+ result_stdout = stdout_data or ""
432
+ result_stderr = stderr_data or ""
433
+
434
+ success = result_returncode == 0 and os.path.exists(metrics_file)
435
+
436
+ if mode == "render":
437
+ found_videos = len(glob.glob(os.path.join(work_dir, "render_results", "*.mp4")))
438
+ expected_videos = num_render_videos or self.config.get("num_render_videos", 64)
439
+ if result_returncode == 0 and found_videos < expected_videos:
440
+ logger.warning(
441
+ f"[{mode}] Fewer videos than requested: found {found_videos}/{expected_videos} "
442
+ f"(OK if dataset has fewer unique motions)"
443
+ )
444
+ elif found_videos == 0:
445
+ logger.error(f"[{mode}] No videos produced")
446
+ success = False
447
+
448
+ if success:
449
+ logger.info(f"[{mode}] Successfully evaluated checkpoint: {checkpoint_path}")
450
+ self.last_evaluation_time = time.time()
451
+ else:
452
+ logger.error(f"[{mode}] Evaluation failed for checkpoint {checkpoint_path}")
453
+ logger.error("=" * 20 + " stdout " + "=" * 20)
454
+ logger.error(result_stdout)
455
+ logger.error("=" * 20 + " stderr " + "=" * 20)
456
+ logger.error(result_stderr)
457
+ logger.error("=" * 20 + " end " + "=" * 20 + "\n")
458
+ if not os.path.exists(metrics_file):
459
+ logger.error(f"[{mode}] Metrics file not found: {metrics_file}")
460
+
461
+ except Exception as e: # noqa: BLE001
462
+ logger.error(f"Error evaluating checkpoint {checkpoint_path}: {e}")
463
+ return False
464
+
465
+ return success
466
+
467
+ def run(self):
468
+ """Main monitoring loop."""
469
+ single_pass = self.config.get("single_pass", False)
470
+ if single_pass:
471
+ logger.info("Running in single-pass mode...")
472
+ else:
473
+ logger.info("Starting checkpoint monitoring loop...")
474
+ eval_datasets = self.config.get("eval_datasets", None)
475
+ eval_modes = self.config.get("eval_modes", [None])
476
+ num_render_videos = self.config.get("num_render_videos", 64)
477
+ num_test_render_videos = self.config.get("num_test_render_videos", 32)
478
+ while not self.shutdown_flag:
479
+ try:
480
+ checkpoints = self.find_checkpoints()
481
+
482
+ new_checkpoints = []
483
+ for cp in checkpoints:
484
+ cp_str = str(cp)
485
+ if (
486
+ self.eval_frequency is not None
487
+ and int(cp.stem.split("_")[-1]) % self.eval_frequency != 0
488
+ ):
489
+ continue
490
+ if cp_str not in self.evaluated_checkpoints and self.is_checkpoint_ready(cp):
491
+ new_checkpoints.append(cp)
492
+
493
+ if self.eval_last_n is not None and len(new_checkpoints) > self.eval_last_n:
494
+ skipped = len(new_checkpoints) - self.eval_last_n
495
+ new_checkpoints = new_checkpoints[-self.eval_last_n :]
496
+ logger.info(
497
+ f"eval_last_n={self.eval_last_n}: skipping {skipped} earlier checkpoints"
498
+ )
499
+
500
+ if single_pass and not new_checkpoints:
501
+ logger.info("Single-pass mode: no new checkpoints to evaluate, exiting")
502
+ break
503
+
504
+ evaluation_success_count = 0
505
+ for checkpoint in new_checkpoints:
506
+ if self.shutdown_flag:
507
+ break
508
+
509
+ eval_step = int(checkpoint.stem.split("_")[-1])
510
+ checkpoint_work_dir = os.path.join(
511
+ self.experiment_dir, "eval", f"{eval_step:06d}"
512
+ )
513
+ os.makedirs(checkpoint_work_dir, exist_ok=True)
514
+ logger.info(f"Found new checkpoint: {checkpoint}")
515
+
516
+ success = True
517
+ metrics_success = True
518
+
519
+ for mode in ["metrics", "render"]:
520
+ mode_work_dir = checkpoint_work_dir + "/train"
521
+ mode_success = self.evaluate_checkpoint(
522
+ checkpoint,
523
+ mode=mode,
524
+ work_dir=mode_work_dir,
525
+ eval_step=eval_step,
526
+ num_render_videos=num_render_videos,
527
+ )
528
+ if mode_success:
529
+ with open(os.path.join(mode_work_dir, f"{mode}_finish.txt"), "w") as f:
530
+ f.write(f"{mode}_finish")
531
+ success = success and mode_success
532
+ if mode == "metrics":
533
+ metrics_success = metrics_success and mode_success
534
+
535
+ if eval_datasets is not None:
536
+ for eval_dataset, eval_mode in itertools.product(eval_datasets, eval_modes):
537
+ for mode in ["metrics", "render"]:
538
+ mode_work_dir = (
539
+ checkpoint_work_dir
540
+ + f"/{Path(eval_dataset).name}_{eval_mode if eval_mode is not None else 'all'}"
541
+ )
542
+ mode_success = self.evaluate_checkpoint(
543
+ checkpoint,
544
+ mode=mode,
545
+ work_dir=mode_work_dir,
546
+ eval_step=eval_step,
547
+ eval_dataset=eval_dataset,
548
+ num_render_videos=num_test_render_videos,
549
+ eval_mode=eval_mode,
550
+ )
551
+ if mode_success:
552
+ with open(
553
+ os.path.join(mode_work_dir, f"{mode}_finish.txt"), "w"
554
+ ) as f:
555
+ f.write(f"{mode}_finish")
556
+ success = success and mode_success
557
+ if mode == "metrics":
558
+ metrics_success = metrics_success and mode_success
559
+
560
+ if success:
561
+ with open(
562
+ os.path.join(checkpoint_work_dir, "all_eval_finish.txt"), "w"
563
+ ) as f:
564
+ f.write("all_eval_finish")
565
+ self._log_eval_to_wandb(eval_step, checkpoint_work_dir)
566
+ elif metrics_success:
567
+ logger.warning(
568
+ f"Render failed for step {eval_step}, logging metrics-only to W&B"
569
+ )
570
+ self._log_eval_to_wandb(eval_step, checkpoint_work_dir)
571
+
572
+ if success:
573
+ self.evaluated_checkpoints.add(str(checkpoint))
574
+ evaluation_success_count += 1
575
+ if eval_step >= self.config.max_train_steps:
576
+ logger.info(
577
+ f"Reached max train steps: {eval_step} >= {self.config.max_train_steps}. Shutting down..." # noqa: E501
578
+ )
579
+ self.shutdown_flag = True
580
+ break
581
+
582
+ if new_checkpoints:
583
+ logger.info(f"Evaluated {evaluation_success_count} new checkpoints")
584
+ logger.info(f"Total evaluated checkpoints: {len(self.evaluated_checkpoints)}")
585
+
586
+ if single_pass:
587
+ logger.info(
588
+ f"Single-pass mode: evaluated {evaluation_success_count} checkpoint(s), exiting"
589
+ )
590
+ break
591
+
592
+ time_since_last_eval = time.time() - self.last_evaluation_time
593
+ if time_since_last_eval > self.evaluation_timeout:
594
+ logger.info(
595
+ f"No checkpoints evaluated in {time_since_last_eval / 3600:.1f} hours. Shutting down..."
596
+ )
597
+ self.shutdown_flag = True
598
+ break
599
+
600
+ time.sleep(self.config.scan_interval)
601
+
602
+ except KeyboardInterrupt:
603
+ logger.info("Received keyboard interrupt. Shutting down...")
604
+ break
605
+ except Exception as e: # noqa: BLE001
606
+ logger.error(f"Error in monitoring loop: {e}")
607
+ time.sleep(self.config.scan_interval)
608
+
609
+ logger.info("Checkpoint monitoring stopped.")
610
+
611
+
612
+ @hydra.main(config_path="config", config_name="eval_exp", version_base="1.1")
613
+ def main(config: omegaconf.OmegaConf) -> None:
614
+ """Main function to start checkpoint monitoring and evaluation."""
615
+ os.chdir(hydra.utils.get_original_cwd())
616
+
617
+ single_pass = config.get("single_pass", False)
618
+
619
+ experiment_dir = Path(config.experiment_dir)
620
+ if not experiment_dir.exists():
621
+ parent_dir = experiment_dir.parent
622
+ prefix = experiment_dir.name
623
+ logger.info(
624
+ f"Experiment directory doesn't exist, looking for prefix match: {prefix}* in {parent_dir}"
625
+ )
626
+
627
+ while True:
628
+ if parent_dir.exists():
629
+ matches = sorted(
630
+ [d for d in parent_dir.iterdir() if d.is_dir() and d.name.startswith(prefix)]
631
+ )[::-1]
632
+ if matches:
633
+ experiment_dir = None
634
+ for match in matches:
635
+ if (match / "meta.yaml").exists():
636
+ experiment_dir = match
637
+ logger.info(
638
+ f"Found matching directory with meta.yaml: {experiment_dir}"
639
+ )
640
+ break
641
+ if experiment_dir is None:
642
+ experiment_dir = matches[-1]
643
+ logger.info(
644
+ f"Found matching directory (no meta.yaml yet): {experiment_dir}"
645
+ )
646
+ config.experiment_dir = str(experiment_dir)
647
+ break
648
+
649
+ if single_pass:
650
+ logger.info("Single-pass mode: no matching directory found, exiting")
651
+ return
652
+
653
+ logger.info("No match found yet, waiting...")
654
+ time.sleep(5)
655
+
656
+ meta_file = os.path.join(config.experiment_dir, "meta.yaml")
657
+ logger.info(f"Waiting for meta.yaml to exist: {meta_file}")
658
+
659
+ if single_pass and not os.path.exists(meta_file):
660
+ logger.info("Single-pass mode: meta.yaml not found, exiting")
661
+ return
662
+ while not os.path.exists(meta_file):
663
+ time.sleep(1)
664
+ meta = yaml.safe_load(open(meta_file)) # noqa: SIM115
665
+ config.max_train_steps = meta["max_train_steps"]
666
+ logger.info(f"Loaded meta: {meta}")
667
+
668
+ hydra_log_path = os.path.join(config.experiment_dir, "eval_exp.log")
669
+ logger.remove()
670
+ logger.add(hydra_log_path, level="DEBUG")
671
+ console_log_level = os.environ.get("LOGURU_LEVEL", "INFO").upper()
672
+ logger.add(sys.stdout, level=console_log_level, colorize=True)
673
+
674
+ evaluator = CheckpointEvaluator(config)
675
+ evaluator.run()
676
+
677
+
678
+ if __name__ == "__main__":
679
+ main()
GR00T-WholeBodyControl/gear_sonic/pyproject.toml ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["setuptools>=67", "wheel", "pip"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "gear_sonic"
7
+ dynamic = ["version"]
8
+ readme = {text = "NVIDIA Gear Sonic - Whole Body Control", content-type = "text/plain"}
9
+ classifiers = [
10
+ "Intended Audience :: Science/Research",
11
+ "Development Status :: 3 - Alpha",
12
+ "License :: OSI Approved :: Apache Software License",
13
+ "Programming Language :: Python :: 3",
14
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
15
+ ]
16
+ authors = [
17
+ {name = "NVIDIA Gear Lab"}
18
+ ]
19
+ requires-python = ">=3.10"
20
+ dependencies = [
21
+ "numpy==1.26.4",
22
+ "scipy==1.15.3",
23
+ "torch>=2.4.0",
24
+ "joblib",
25
+ "tqdm",
26
+ "easydict",
27
+ "loguru",
28
+ ]
29
+ license = {text = "Apache-2.0"}
30
+
31
+ [project.optional-dependencies]
32
+ # Teleop: minimal deps for running ZMQ-based teleop
33
+ # pico_manager_thread_server.py + visualization (pyzmq, pyvista, pinocchio FK)
34
+ # Usage: pip install -e "gear_sonic[teleop]"
35
+ teleop = [
36
+ "pyzmq",
37
+ "msgpack",
38
+ "msgpack-numpy",
39
+ "pin",
40
+ "pyvista; platform_machine != 'aarch64'",
41
+ ]
42
+ # MuJoCo simulation (run_sim_loop.py and related scripts)
43
+ # Usage: pip install -e "gear_sonic[sim]"
44
+ sim = [
45
+ "mujoco",
46
+ "tyro",
47
+ "pin",
48
+ "pyyaml",
49
+ "pyzmq",
50
+ "msgpack",
51
+ "msgpack-numpy",
52
+ "opencv-python",
53
+ ]
54
+ # Data collection: Sonic VLA data exporter with LeRobot dataset output
55
+ # Usage: pip install -e "gear_sonic[data_collection]"
56
+ data_collection = [
57
+ "pyzmq",
58
+ "msgpack",
59
+ "msgpack-numpy",
60
+ "pin",
61
+ "tyro",
62
+ "pyttsx3==2.90",
63
+ "av>=14.2",
64
+ "opencv-python",
65
+ "lerobot @ git+https://github.com/huggingface/lerobot.git@a445d9c9da6bea99a8972daa4fe1fdd053d711d2",
66
+ "datasets==3.6.0",
67
+ ]
68
+ # Camera server: runs on the robot to publish camera frames over ZMQ
69
+ # Includes depthai (OAK cameras) by default. For other SDKs (pyrealsense2),
70
+ # install them separately into the .venv_camera venv.
71
+ # Usage: pip install -e "gear_sonic[camera]"
72
+ camera = [
73
+ "pyzmq",
74
+ "msgpack",
75
+ "msgpack-numpy",
76
+ "opencv-python",
77
+ "tyro",
78
+ "depthai",
79
+ "requests",
80
+ ]
81
+ # Inference: VLA inference client for running Isaac-GR00T policies
82
+ # Usage: pip install -e "gear_sonic[inference]"
83
+ inference = [
84
+ "pyzmq",
85
+ "msgpack",
86
+ "msgpack-numpy",
87
+ "pin",
88
+ "tyro",
89
+ "opencv-python",
90
+ "scipy",
91
+ "Isaac-GR00T @ git+https://github.com/NVIDIA/Isaac-GR00T.git",
92
+ ]
93
+ # Training: full RL training stack (Isaac Lab must be installed separately)
94
+ # Usage: pip install -e "gear_sonic[training]"
95
+ training = [
96
+ "hydra-core==1.3.2",
97
+ "wandb",
98
+ "trl==0.28.0",
99
+ "transformers>=4.56.2",
100
+ "accelerate>=1.3.0",
101
+ "tensorboard",
102
+ "smpl_sim @ git+https://github.com/ZhengyiLuo/SMPLSim.git",
103
+ ]
104
+
105
+ [tool.setuptools.packages.find]
106
+ where = [".."]
107
+ include = ["gear_sonic*"]
108
+
109
+ [tool.setuptools]
110
+ include-package-data = true
111
+
112
+ [tool.setuptools.package-data]
113
+ gear_sonic = ["py.typed", "**/*.json", "**/*.yaml"]
114
+
115
+ [tool.setuptools.dynamic]
116
+ version = {attr = "gear_sonic.version.VERSION"}
GR00T-WholeBodyControl/gear_sonic/scripts/launch_data_collection.py ADDED
@@ -0,0 +1,473 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ All-in-one tmux launcher for SONIC data collection.
3
+
4
+ Starts the full data collection stack in a single tmux session:
5
+
6
+ Window 0 — data_collection (4 panes):
7
+ ┌───────────────────────┬───────────────────────┐
8
+ │ Pane 0: C++ Deploy │ Pane 2: Data Exporter │
9
+ │ (gear_sonic_deploy) │ (.venv_data_collection)│
10
+ ├───────────────────────┼───────────────────────┤
11
+ │ Pane 1: Teleop │ Pane 3: Camera Viewer │
12
+ │ (.venv_teleop) │ (.venv_data_collection)│
13
+ └───────────────────────┴───────────────────────┘
14
+
15
+ Window 1 — sim (only when --sim is passed):
16
+ ┌─────────────────────────────────────────────────┐
17
+ │ MuJoCo Simulator (run_sim_loop.py) │
18
+ │ (.venv_sim) │
19
+ └─────────────────────────────────────────────────┘
20
+
21
+ Prerequisites:
22
+ - tmux installed (sudo apt install tmux)
23
+ - Virtual environments set up:
24
+ bash install_scripts/install_pico.sh -> .venv_teleop
25
+ bash install_scripts/install_data_collection.sh -> .venv_data_collection
26
+ - gear_sonic_deploy built (see docs)
27
+ - For sim: .venv_sim must exist (see install instructions)
28
+
29
+ Usage (from repo root — no venv activation needed):
30
+ python gear_sonic/scripts/launch_data_collection.py # real robot (default)
31
+ python gear_sonic/scripts/launch_data_collection.py --sim # MuJoCo sim
32
+ python gear_sonic/scripts/launch_data_collection.py --no-camera-viewer # skip viewer
33
+ python gear_sonic/scripts/launch_data_collection.py --pico-input-source isaac-teleop # in-process CloudXR / DeviceIO
34
+ """
35
+
36
+ from dataclasses import dataclass
37
+ from pathlib import Path
38
+ import os
39
+ import shutil
40
+ import signal
41
+ import socket
42
+ import subprocess
43
+ import sys
44
+ import time
45
+
46
+
47
+ def _bootstrap_venv():
48
+ """Re-exec with the .venv_data_collection Python if tyro is not available."""
49
+ try:
50
+ import tyro # noqa: F401
51
+ return
52
+ except ImportError:
53
+ pass
54
+
55
+ repo_root = Path(__file__).resolve().parent.parent.parent
56
+ venv_python = repo_root / ".venv_data_collection" / "bin" / "python"
57
+ if not venv_python.exists():
58
+ print(
59
+ "ERROR: tyro is not installed and .venv_data_collection not found.\n"
60
+ " Run: bash install_scripts/install_data_collection.sh"
61
+ )
62
+ sys.exit(1)
63
+
64
+ print(f"Re-launching with {venv_python} ...")
65
+ os.execv(str(venv_python), [str(venv_python)] + sys.argv)
66
+
67
+
68
+ _bootstrap_venv()
69
+
70
+ import tyro
71
+
72
+
73
+ def _get_local_ip() -> str:
74
+ """Best-effort detection of the PC's LAN IP address."""
75
+ try:
76
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
77
+ s.connect(("8.8.8.8", 80))
78
+ ip = s.getsockname()[0]
79
+ s.close()
80
+ return ip
81
+ except Exception:
82
+ return "unknown"
83
+
84
+
85
+ @dataclass
86
+ class DataCollectionLaunchConfig:
87
+ """CLI config for the all-in-one data collection tmux launcher."""
88
+
89
+ # Deployment mode
90
+ sim: bool = False
91
+ """Run against MuJoCo sim (deploy.sh sim) instead of real robot."""
92
+
93
+ # C++ deploy options
94
+ deploy_input_type: str = "zmq_manager"
95
+ """Input type for the C++ deploy (zmq_manager, keyboard, etc.)."""
96
+
97
+ deploy_zmq_host: str = "localhost"
98
+ """ZMQ host for the C++ deploy to listen on."""
99
+
100
+ deploy_checkpoint: str = ""
101
+ """Checkpoint path for deploy.sh (e.g., 'policy/checkpoints/my_model/model_step_100000').
102
+ Leave empty to use the deploy.sh default."""
103
+
104
+ deploy_obs_config: str = ""
105
+ """Observation config file for deploy.sh. Leave empty for default."""
106
+
107
+ deploy_planner: str = ""
108
+ """Planner model path for deploy.sh. Leave empty for default."""
109
+
110
+ deploy_motion_data: str = ""
111
+ """Motion data path for deploy.sh. Leave empty for default."""
112
+
113
+ deploy_output_type: str = ""
114
+ """Output type for deploy.sh. Leave empty for default."""
115
+
116
+ # Teleop streamer options
117
+ pico_manager: bool = True
118
+ """Run pico_manager_thread_server with --manager flag."""
119
+
120
+ pico_input_source: str = "xrt"
121
+ """Teleop input source for pico_manager_thread_server.py (xrt or isaac-teleop)."""
122
+
123
+ pico_vis_vr3pt: bool = False
124
+ """Enable VR 3-point visualization on the teleop streamer."""
125
+
126
+ pico_vis_smpl: bool = False
127
+ """Enable SMPL visualization on the teleop streamer."""
128
+
129
+ pico_waist_tracking: bool = False
130
+ """Enable waist tracking on the teleop streamer."""
131
+
132
+ # Data exporter options
133
+ task_prompt: str = "demo"
134
+ """Language task prompt for the data exporter."""
135
+
136
+ dataset_name: str = ""
137
+ """Dataset name for the data exporter. Leave empty to auto-generate from timestamp."""
138
+
139
+ data_exporter_frequency: int = 50
140
+ """Data collection frequency (Hz) for the data exporter."""
141
+
142
+ record_wrist_cameras: bool = False
143
+ """Record wrist camera streams (left_wrist, right_wrist) in the dataset."""
144
+
145
+ text_to_speech: bool = True
146
+ """Enable voice feedback via espeak (data exporter)."""
147
+
148
+ # Camera viewer
149
+ camera_viewer: bool = True
150
+ """Start the camera viewer pane."""
151
+
152
+ camera_host: str = "localhost"
153
+ """Camera server host (shared by data exporter and viewer)."""
154
+
155
+ camera_port: int = 5555
156
+ """Camera server port (shared by data exporter and viewer)."""
157
+
158
+
159
+ SESSION_NAME = "sonic_data_collection"
160
+
161
+
162
+ def _check_prerequisites(config: DataCollectionLaunchConfig):
163
+ """Verify that required tools and venvs exist."""
164
+ errors = []
165
+
166
+ if not shutil.which("tmux"):
167
+ errors.append("tmux is not installed. Install with: sudo apt install tmux")
168
+
169
+ repo_root = Path(__file__).resolve().parent.parent.parent
170
+
171
+ if not (repo_root / ".venv_teleop" / "bin" / "activate").exists():
172
+ errors.append(
173
+ ".venv_teleop not found. Run: bash install_scripts/install_pico.sh"
174
+ )
175
+
176
+ if not (repo_root / ".venv_data_collection" / "bin" / "activate").exists():
177
+ errors.append(
178
+ ".venv_data_collection not found. Run: "
179
+ "bash install_scripts/install_data_collection.sh"
180
+ )
181
+
182
+ deploy_dir = repo_root / "gear_sonic_deploy"
183
+ if not (deploy_dir / "deploy.sh").exists():
184
+ errors.append(
185
+ f"gear_sonic_deploy/deploy.sh not found at {deploy_dir}. "
186
+ "Ensure the deploy directory is set up."
187
+ )
188
+
189
+ if config.sim and not (repo_root / ".venv_sim" / "bin" / "activate").exists():
190
+ errors.append(
191
+ ".venv_sim not found. Set up the simulation venv first "
192
+ "(see install instructions)."
193
+ )
194
+
195
+ if config.pico_input_source not in {"xrt", "isaac-teleop"}:
196
+ errors.append("--pico-input-source must be one of: xrt, isaac-teleop")
197
+
198
+ if errors:
199
+ print("ERROR: Prerequisites not met:\n")
200
+ for e in errors:
201
+ print(f" - {e}")
202
+ print()
203
+ sys.exit(1)
204
+
205
+
206
+ def _kill_existing_session():
207
+ """Kill any existing tmux session with our name."""
208
+ subprocess.run(
209
+ ["tmux", "kill-session", "-t", SESSION_NAME],
210
+ capture_output=True,
211
+ )
212
+
213
+
214
+ def _create_tmux_session():
215
+ """Create a 4-pane tmux layout."""
216
+ # Create detached session
217
+ subprocess.run(
218
+ ["tmux", "new-session", "-d", "-s", SESSION_NAME],
219
+ check=True,
220
+ )
221
+
222
+ # Enable mouse support (click panes, scroll, resize)
223
+ subprocess.run(
224
+ ["tmux", "set-option", "-t", SESSION_NAME, "-g", "mouse", "on"],
225
+ )
226
+
227
+ # Bind Ctrl+\ to kill the entire session (no prefix needed)
228
+ subprocess.run(
229
+ ["tmux", "bind-key", "-T", "root", "C-\\", "kill-session"],
230
+ )
231
+
232
+ # Rename default window
233
+ subprocess.run(
234
+ ["tmux", "rename-window", "-t", f"{SESSION_NAME}:0", "data_collection"],
235
+ )
236
+
237
+ # Split into 4 panes:
238
+ # 0 | 1
239
+ # -----
240
+ # 2 | 3
241
+
242
+ # Split horizontally: pane 0 (left) and pane 1 (right)
243
+ subprocess.run(
244
+ ["tmux", "split-window", "-t", f"{SESSION_NAME}:0", "-h"],
245
+ )
246
+
247
+ # Split left pane vertically: pane 0 (top-left) and pane 2 (bottom-left)
248
+ subprocess.run(
249
+ ["tmux", "split-window", "-t", f"{SESSION_NAME}:0.0", "-v"],
250
+ )
251
+
252
+ # Split right pane vertically: pane 1 becomes top-right, new pane 3 bottom-right
253
+ subprocess.run(
254
+ ["tmux", "split-window", "-t", f"{SESSION_NAME}:0.2", "-v"],
255
+ )
256
+
257
+ # Let all pane shells finish initialization (.bashrc, conda, etc.)
258
+ time.sleep(5)
259
+
260
+
261
+ def _send_to_pane(pane_index: int, cmd: str, wait: float = 1.0):
262
+ """Send a command string to a tmux pane."""
263
+ target = f"{SESSION_NAME}:0.{pane_index}"
264
+
265
+ subprocess.run(
266
+ ["tmux", "send-keys", "-t", target, cmd, "C-m"],
267
+ )
268
+ time.sleep(wait)
269
+
270
+
271
+ def _check_pane_alive(pane_index: int) -> bool:
272
+ """Check if a tmux pane's process is still running."""
273
+ target = f"{SESSION_NAME}:0.{pane_index}"
274
+ result = subprocess.run(
275
+ ["tmux", "list-panes", "-t", target, "-F", "#{pane_dead}"],
276
+ capture_output=True,
277
+ text=True,
278
+ )
279
+ return result.stdout.strip() != "1"
280
+
281
+
282
+ def main(config: DataCollectionLaunchConfig):
283
+ repo_root = Path(__file__).resolve().parent.parent.parent
284
+
285
+ _check_prerequisites(config)
286
+ _kill_existing_session()
287
+
288
+ print("=" * 60)
289
+ print(" SONIC Data Collection Launcher")
290
+ print("=" * 60)
291
+ print(f" Mode: {'Simulation' if config.sim else 'Real Robot'}")
292
+ print(f" Task prompt: {config.task_prompt}")
293
+ print(f" Dataset name: {config.dataset_name or '(auto)'}")
294
+ print(f" Deploy input: {config.deploy_input_type}")
295
+ print(f" Teleop input: {config.pico_input_source}")
296
+ if config.deploy_checkpoint:
297
+ print(f" Checkpoint: {config.deploy_checkpoint}")
298
+ print(f" Camera: {config.camera_host}:{config.camera_port}")
299
+ print(f" DC frequency: {config.data_exporter_frequency} Hz")
300
+ print(f" Camera viewer: {'Yes' if config.camera_viewer else 'No'}")
301
+ print(f" Wrist cameras: {'Yes' if config.record_wrist_cameras else 'No'}")
302
+ print(f" Text-to-speech: {'Yes' if config.text_to_speech else 'No'}")
303
+ print(f" PC IP (for PICO): {_get_local_ip()}")
304
+ print(f" Teleop vis: vr3pt={config.pico_vis_vr3pt} smpl={config.pico_vis_smpl}")
305
+ print("=" * 60)
306
+
307
+ _create_tmux_session()
308
+ print(f"Created tmux session: {SESSION_NAME}")
309
+
310
+ # --- Window 1 (sim only): MuJoCo Simulator ---
311
+ if config.sim:
312
+ subprocess.run(
313
+ ["tmux", "new-window", "-t", SESSION_NAME, "-n", "sim"],
314
+ )
315
+ sim_cmd = (
316
+ f"cd {repo_root} && "
317
+ f"source .venv_sim/bin/activate && "
318
+ f"python gear_sonic/scripts/run_sim_loop.py "
319
+ f"--enable-image-publish --enable-offscreen "
320
+ f"--camera-port {config.camera_port}"
321
+ )
322
+ sim_target = f"{SESSION_NAME}:sim"
323
+ subprocess.run(
324
+ ["tmux", "send-keys", "-t", sim_target, sim_cmd, "C-m"],
325
+ )
326
+ print("Starting MuJoCo simulator (window: sim)...")
327
+ time.sleep(3.0)
328
+
329
+ # Switch back to the data_collection window for the remaining panes
330
+ subprocess.run(
331
+ ["tmux", "select-window", "-t", f"{SESSION_NAME}:data_collection"],
332
+ )
333
+
334
+ # --- Pane 0 (top-left): C++ Deploy ---
335
+ deploy_mode = "sim" if config.sim else "real"
336
+ deploy_cmd = (
337
+ f"cd {repo_root / 'gear_sonic_deploy'} && "
338
+ f"./deploy.sh "
339
+ f"--input-type {config.deploy_input_type} "
340
+ f"--zmq-host {config.deploy_zmq_host} "
341
+ )
342
+ if config.deploy_checkpoint:
343
+ deploy_cmd += f"--cp {config.deploy_checkpoint} "
344
+ if config.deploy_obs_config:
345
+ deploy_cmd += f"--obs-config {config.deploy_obs_config} "
346
+ if config.deploy_planner:
347
+ deploy_cmd += f"--planner {config.deploy_planner} "
348
+ if config.deploy_motion_data:
349
+ deploy_cmd += f"--motion-data {config.deploy_motion_data} "
350
+ if config.deploy_output_type:
351
+ deploy_cmd += f"--output-type {config.deploy_output_type} "
352
+ deploy_cmd += deploy_mode
353
+
354
+ print("Starting C++ deploy (pane 0)...")
355
+ _send_to_pane(0, deploy_cmd, wait=3.0)
356
+
357
+ if not _check_pane_alive(0):
358
+ print("WARNING: C++ deploy pane may have failed to start.")
359
+
360
+ # --- Pane 2 (bottom-left): Teleop Streamer ---
361
+ pico_cmd = (
362
+ f"cd {repo_root} && "
363
+ f"source .venv_teleop/bin/activate && "
364
+ f"python gear_sonic/scripts/pico_manager_thread_server.py "
365
+ f"--input-source {config.pico_input_source}"
366
+ )
367
+ if config.pico_manager:
368
+ pico_cmd += " --manager"
369
+ if config.pico_vis_vr3pt:
370
+ pico_cmd += " --vis_vr3pt"
371
+ if config.pico_vis_smpl:
372
+ pico_cmd += " --vis_smpl"
373
+ if config.pico_waist_tracking:
374
+ pico_cmd += " --waist_tracking"
375
+
376
+ print("Starting teleop streamer (pane 2)...")
377
+ _send_to_pane(1, pico_cmd, wait=2.0)
378
+
379
+ # --- Pane 3 (bottom-right): Camera Viewer ---
380
+ if config.camera_viewer:
381
+ viewer_cmd = (
382
+ f"cd {repo_root} && "
383
+ f"source .venv_data_collection/bin/activate && "
384
+ f"python gear_sonic/scripts/run_camera_viewer.py "
385
+ f"--camera-host {config.camera_host} "
386
+ f"--camera-port {config.camera_port}"
387
+ )
388
+ print("Starting camera viewer (pane 3)...")
389
+ _send_to_pane(3, viewer_cmd, wait=2.0)
390
+
391
+ # --- Pane 1 (top-right): Data Exporter ---
392
+ exporter_cmd = (
393
+ f"cd {repo_root} && "
394
+ f"source .venv_data_collection/bin/activate && "
395
+ f"python gear_sonic/scripts/run_data_exporter.py "
396
+ f"--task-prompt '{config.task_prompt}' "
397
+ f"--data-collection-frequency {config.data_exporter_frequency} "
398
+ f"--camera-host {config.camera_host} "
399
+ f"--camera-port {config.camera_port}"
400
+ )
401
+ if config.dataset_name:
402
+ exporter_cmd += f" --dataset-name '{config.dataset_name}'"
403
+ if config.record_wrist_cameras:
404
+ exporter_cmd += " --record-wrist-cameras"
405
+ if not config.text_to_speech:
406
+ exporter_cmd += " --no-text-to-speech"
407
+
408
+ print("Starting data exporter (pane 1)...")
409
+ _send_to_pane(2, exporter_cmd, wait=1.0)
410
+
411
+ # Select the data exporter pane so the user lands there for interactive input
412
+ subprocess.run(
413
+ ["tmux", "select-pane", "-t", f"{SESSION_NAME}:0.2"],
414
+ )
415
+
416
+ print()
417
+ print("=" * 60)
418
+ print(" All components launched!")
419
+ print()
420
+ print(f" tmux session: {SESSION_NAME}")
421
+ print()
422
+ if config.sim:
423
+ print(" Window 'sim':")
424
+ print(" MuJoCo Simulator (.venv_sim)")
425
+ print()
426
+ print(" Window 'data_collection':")
427
+ print(" Pane 0 (top-left): C++ Deploy")
428
+ print(" Pane 1 (bottom-left): Teleop Streamer")
429
+ print(" Pane 2 (top-right): Data Exporter <-- you are here")
430
+ if config.camera_viewer:
431
+ print(" Pane 3 (bottom-right): Camera Viewer")
432
+ print()
433
+ print(" ** deploy.sh (pane 0) is waiting for confirmation —")
434
+ print(" click on pane 0 and press Enter to proceed **")
435
+ print()
436
+ print(" Controls:")
437
+ print(" Ctrl+b, arrow keys - Switch between panes")
438
+ if config.sim:
439
+ print(" Ctrl+b, n / p - Next / previous window")
440
+ print(" Ctrl+b, d - Detach from session")
441
+ print(" Ctrl+\\ - Kill entire session")
442
+ print("=" * 60)
443
+
444
+ # Attach to the session
445
+ try:
446
+ subprocess.run(["tmux", "attach", "-t", SESSION_NAME])
447
+ except KeyboardInterrupt:
448
+ pass
449
+
450
+ # After detach/exit, offer cleanup
451
+ result = subprocess.run(
452
+ ["tmux", "has-session", "-t", SESSION_NAME],
453
+ capture_output=True,
454
+ )
455
+ if result.returncode == 0:
456
+ print(f"\nSession '{SESSION_NAME}' is still running.")
457
+ print(f" Reattach: tmux attach -t {SESSION_NAME}")
458
+ print(f" Kill: tmux kill-session -t {SESSION_NAME}")
459
+
460
+
461
+ def _signal_handler(sig, frame):
462
+ print("\nShutdown requested...")
463
+ subprocess.run(
464
+ ["tmux", "kill-session", "-t", SESSION_NAME],
465
+ capture_output=True,
466
+ )
467
+ sys.exit(0)
468
+
469
+
470
+ if __name__ == "__main__":
471
+ signal.signal(signal.SIGINT, _signal_handler)
472
+ config = tyro.cli(DataCollectionLaunchConfig)
473
+ main(config)
GR00T-WholeBodyControl/gear_sonic/scripts/launch_inference.py ADDED
@@ -0,0 +1,465 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ All-in-one tmux launcher for SONIC VLA inference.
3
+
4
+ Starts the inference stack in a single tmux session:
5
+
6
+ Window 0 — inference (4 panes):
7
+ ┌───────────────────────┬───────────────────────┐
8
+ │ Pane 0: C++ Deploy │ Pane 1: VLA Inference │
9
+ │ (gear_sonic_deploy) │ (.venv_inference) │
10
+ ├───────────────────────┼───────────────────────┤
11
+ │ Pane 2: Keyboard Pub │ Pane 3: Data Exporter │
12
+ │ (.venv_inference) │ (.venv_data_collection)│
13
+ └───────────────────────┴───────────────────────┘
14
+
15
+ Window 1 — sim (only when --sim is passed):
16
+ ┌─────────────────────────────────────────────────┐
17
+ │ MuJoCo Simulator (run_sim_loop.py) │
18
+ │ (.venv_sim) │
19
+ └─────────────────────────────────────────────────┘
20
+
21
+ Prerequisites:
22
+ - tmux installed (sudo apt install tmux)
23
+ - Virtual environments set up:
24
+ bash install_scripts/install_inference.sh -> .venv_inference
25
+ bash install_scripts/install_data_collection.sh -> .venv_data_collection (optional, for recording)
26
+ - gear_sonic_deploy built (see docs)
27
+ - Isaac-GR00T PolicyServer running separately
28
+
29
+ Usage (from repo root — no venv activation needed):
30
+ python gear_sonic/scripts/launch_inference.py # real robot
31
+ python gear_sonic/scripts/launch_inference.py --sim # MuJoCo sim
32
+ python gear_sonic/scripts/launch_inference.py --no-data-exporter # no recording pane
33
+ """
34
+
35
+ from dataclasses import dataclass
36
+ from pathlib import Path
37
+ import os
38
+ import shutil
39
+ import signal
40
+ import socket
41
+ import base64
42
+ import subprocess
43
+ import sys
44
+ import textwrap
45
+ import time
46
+
47
+
48
+ def _bootstrap_venv():
49
+ """Re-exec with the .venv_inference Python if tyro is not available."""
50
+ try:
51
+ import tyro # noqa: F401
52
+ return
53
+ except ImportError:
54
+ pass
55
+
56
+ repo_root = Path(__file__).resolve().parent.parent.parent
57
+ venv_python = repo_root / ".venv_inference" / "bin" / "python"
58
+ if not venv_python.exists():
59
+ print(
60
+ "ERROR: tyro is not installed and .venv_inference not found.\n"
61
+ " Run: bash install_scripts/install_inference.sh"
62
+ )
63
+ sys.exit(1)
64
+
65
+ print(f"Re-launching with {venv_python} ...")
66
+ os.execv(str(venv_python), [str(venv_python)] + sys.argv)
67
+
68
+
69
+ _bootstrap_venv()
70
+
71
+ import tyro
72
+
73
+
74
+ def _get_local_ip() -> str:
75
+ """Best-effort detection of the PC's LAN IP address."""
76
+ try:
77
+ s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
78
+ s.connect(("8.8.8.8", 80))
79
+ ip = s.getsockname()[0]
80
+ s.close()
81
+ return ip
82
+ except Exception:
83
+ return "unknown"
84
+
85
+
86
+ @dataclass
87
+ class InferenceLaunchConfig:
88
+ """CLI config for the all-in-one VLA inference tmux launcher."""
89
+
90
+ # Deployment mode
91
+ sim: bool = False
92
+ """Run against MuJoCo sim instead of real robot."""
93
+
94
+ # C++ deploy options
95
+ deploy_input_type: str = "zmq_manager"
96
+ """Input type for the C++ deploy."""
97
+
98
+ deploy_zmq_host: str = "localhost"
99
+ """ZMQ host for the C++ deploy to listen on."""
100
+
101
+ deploy_checkpoint: str = ""
102
+ """Checkpoint path for deploy.sh. Leave empty for default."""
103
+
104
+ deploy_obs_config: str = ""
105
+ """Observation config file for deploy.sh. Leave empty for default."""
106
+
107
+ deploy_planner: str = ""
108
+ """Planner model path for deploy.sh. Leave empty for default."""
109
+
110
+ deploy_motion_data: str = ""
111
+ """Motion data path for deploy.sh. Leave empty for default."""
112
+
113
+ deploy_output_type: str = ""
114
+ """Output type for deploy.sh. Leave empty for default."""
115
+
116
+ # VLA inference options
117
+ policy_host: str = "localhost"
118
+ """Isaac-GR00T PolicyServer host."""
119
+
120
+ policy_port: int = 5550
121
+ """Isaac-GR00T PolicyServer port."""
122
+
123
+ embodiment_tag: str = "unitree_g1_sonic"
124
+ """Embodiment tag for policy inference."""
125
+
126
+ prompt: str = "demo"
127
+ """Language prompt for inference."""
128
+
129
+ action_publish_rate: int = 50
130
+ """Rate at which individual actions are published to the C++ control loop (Hz)."""
131
+
132
+ action_horizon: int = 40
133
+ """Action horizon of the VLA policy."""
134
+
135
+ # Camera
136
+ camera_host: str = "localhost"
137
+ """Camera server host."""
138
+
139
+ camera_port: int = 5555
140
+ """Camera server port."""
141
+
142
+ # Data exporter (optional recording during inference)
143
+ data_exporter: bool = True
144
+ """Start the data exporter pane for recording during inference."""
145
+
146
+ data_exporter_frequency: int = 50
147
+ """Data collection frequency (Hz) for the data exporter."""
148
+
149
+ task_prompt: str = ""
150
+ """Task prompt for the data exporter. Defaults to the inference prompt if empty."""
151
+
152
+ dataset_name: str = ""
153
+ """Dataset name for the data exporter. Leave empty to auto-generate."""
154
+
155
+
156
+ SESSION_NAME = "sonic_inference"
157
+
158
+
159
+ def _check_prerequisites(config: InferenceLaunchConfig):
160
+ """Verify that required tools and venvs exist."""
161
+ errors = []
162
+
163
+ if not shutil.which("tmux"):
164
+ errors.append("tmux is not installed. Install with: sudo apt install tmux")
165
+
166
+ repo_root = Path(__file__).resolve().parent.parent.parent
167
+
168
+ if not (repo_root / ".venv_inference" / "bin" / "activate").exists():
169
+ errors.append(
170
+ ".venv_inference not found. Run: bash install_scripts/install_inference.sh"
171
+ )
172
+
173
+ deploy_dir = repo_root / "gear_sonic_deploy"
174
+ if not (deploy_dir / "deploy.sh").exists():
175
+ errors.append(
176
+ f"gear_sonic_deploy/deploy.sh not found at {deploy_dir}. "
177
+ "Ensure the deploy directory is set up."
178
+ )
179
+
180
+ if config.data_exporter:
181
+ if not (repo_root / ".venv_data_collection" / "bin" / "activate").exists():
182
+ errors.append(
183
+ ".venv_data_collection not found (needed for data exporter). Run: "
184
+ "bash install_scripts/install_data_collection.sh"
185
+ )
186
+
187
+ if config.sim and not (repo_root / ".venv_sim" / "bin" / "activate").exists():
188
+ errors.append(
189
+ ".venv_sim not found. Set up the simulation venv first."
190
+ )
191
+
192
+ if errors:
193
+ print("ERROR: Prerequisites not met:\n")
194
+ for e in errors:
195
+ print(f" - {e}")
196
+ print()
197
+ sys.exit(1)
198
+
199
+
200
+ def _kill_existing_session():
201
+ subprocess.run(
202
+ ["tmux", "kill-session", "-t", SESSION_NAME],
203
+ capture_output=True,
204
+ )
205
+
206
+
207
+ def _create_tmux_session():
208
+ subprocess.run(
209
+ ["tmux", "new-session", "-d", "-s", SESSION_NAME],
210
+ check=True,
211
+ )
212
+ subprocess.run(
213
+ ["tmux", "set-option", "-t", SESSION_NAME, "-g", "mouse", "on"],
214
+ )
215
+ subprocess.run(
216
+ ["tmux", "bind-key", "-T", "root", "C-\\", "kill-session"],
217
+ )
218
+ subprocess.run(
219
+ ["tmux", "rename-window", "-t", f"{SESSION_NAME}:0", "inference"],
220
+ )
221
+
222
+ # Split into 4 panes: 0|1 / 2|3
223
+ subprocess.run(
224
+ ["tmux", "split-window", "-t", f"{SESSION_NAME}:0", "-h"],
225
+ )
226
+ subprocess.run(
227
+ ["tmux", "split-window", "-t", f"{SESSION_NAME}:0.0", "-v"],
228
+ )
229
+ subprocess.run(
230
+ ["tmux", "split-window", "-t", f"{SESSION_NAME}:0.2", "-v"],
231
+ )
232
+
233
+ time.sleep(5)
234
+
235
+
236
+ def _send_to_pane(pane_index: int, cmd: str, wait: float = 1.0):
237
+ target = f"{SESSION_NAME}:0.{pane_index}"
238
+ subprocess.run(
239
+ ["tmux", "send-keys", "-t", target, cmd, "C-m"],
240
+ )
241
+ time.sleep(wait)
242
+
243
+
244
+ def _check_pane_alive(pane_index: int) -> bool:
245
+ target = f"{SESSION_NAME}:0.{pane_index}"
246
+ result = subprocess.run(
247
+ ["tmux", "list-panes", "-t", target, "-F", "#{pane_dead}"],
248
+ capture_output=True,
249
+ text=True,
250
+ )
251
+ return result.stdout.strip() != "1"
252
+
253
+
254
+ def main(config: InferenceLaunchConfig):
255
+ repo_root = Path(__file__).resolve().parent.parent.parent
256
+
257
+ _check_prerequisites(config)
258
+ _kill_existing_session()
259
+
260
+ exporter_prompt = config.task_prompt if config.task_prompt else config.prompt
261
+
262
+ print("=" * 60)
263
+ print(" SONIC VLA Inference Launcher")
264
+ print("=" * 60)
265
+ print(f" Mode: {'Simulation' if config.sim else 'Real Robot'}")
266
+ print(f" PolicyServer: {config.policy_host}:{config.policy_port}")
267
+ print(f" Embodiment: {config.embodiment_tag}")
268
+ print(f" Prompt: {config.prompt}")
269
+ print(f" Action rate: {config.action_publish_rate} Hz")
270
+ print(f" Action horizon: {config.action_horizon}")
271
+ print(f" Camera: {config.camera_host}:{config.camera_port}")
272
+ print(f" Data exporter: {'Yes' if config.data_exporter else 'No'}")
273
+ if config.data_exporter:
274
+ print(f" DC frequency: {config.data_exporter_frequency} Hz")
275
+ print(f" Task prompt: {exporter_prompt}")
276
+ print(f" PC IP: {_get_local_ip()}")
277
+ print("=" * 60)
278
+
279
+ _create_tmux_session()
280
+ print(f"Created tmux session: {SESSION_NAME}")
281
+
282
+ # --- Window 1 (sim only): MuJoCo Simulator ---
283
+ if config.sim:
284
+ subprocess.run(
285
+ ["tmux", "new-window", "-t", SESSION_NAME, "-n", "sim"],
286
+ )
287
+ sim_cmd = (
288
+ f"cd {repo_root} && "
289
+ f"source .venv_sim/bin/activate && "
290
+ f"python gear_sonic/scripts/run_sim_loop.py "
291
+ f"--enable-image-publish --enable-offscreen "
292
+ f"--camera-port {config.camera_port}"
293
+ )
294
+ sim_target = f"{SESSION_NAME}:sim"
295
+ subprocess.run(
296
+ ["tmux", "send-keys", "-t", sim_target, sim_cmd, "C-m"],
297
+ )
298
+ print("Starting MuJoCo simulator (window: sim)...")
299
+ time.sleep(3.0)
300
+
301
+ subprocess.run(
302
+ ["tmux", "select-window", "-t", f"{SESSION_NAME}:inference"],
303
+ )
304
+
305
+ # --- Pane 0 (top-left): C++ Deploy ---
306
+ deploy_mode = "sim" if config.sim else "real"
307
+ deploy_cmd = (
308
+ f"cd {repo_root / 'gear_sonic_deploy'} && "
309
+ f"./deploy.sh "
310
+ f"--input-type {config.deploy_input_type} "
311
+ f"--zmq-host {config.deploy_zmq_host} "
312
+ )
313
+ if config.deploy_checkpoint:
314
+ deploy_cmd += f"--cp {config.deploy_checkpoint} "
315
+ if config.deploy_obs_config:
316
+ deploy_cmd += f"--obs-config {config.deploy_obs_config} "
317
+ if config.deploy_planner:
318
+ deploy_cmd += f"--planner {config.deploy_planner} "
319
+ if config.deploy_motion_data:
320
+ deploy_cmd += f"--motion-data {config.deploy_motion_data} "
321
+ if config.deploy_output_type:
322
+ deploy_cmd += f"--output-type {config.deploy_output_type} "
323
+ deploy_cmd += deploy_mode
324
+
325
+ print("Starting C++ deploy (pane 0)...")
326
+ _send_to_pane(0, deploy_cmd, wait=3.0)
327
+
328
+ if not _check_pane_alive(0):
329
+ print("WARNING: C++ deploy pane may have failed to start.")
330
+
331
+ # --- Pane 2 (bottom-left): Keyboard Publisher ---
332
+ keyboard_script = textwrap.dedent("""\
333
+ import zmq, time
334
+ ctx = zmq.Context()
335
+ pub = ctx.socket(zmq.PUB)
336
+ pub.bind('tcp://localhost:5580')
337
+ time.sleep(0.5)
338
+ print('Keyboard publisher ready. Keys: p=pause, k=start/stop, i=init pose, [/]=toggle hands, t=prompt')
339
+ while True:
340
+ key = input()
341
+ if key.startswith('t '):
342
+ pub.send_string('prompt:' + key[2:])
343
+ print('Sent prompt: ' + key[2:])
344
+ else:
345
+ pub.send_string(key)
346
+ print('Sent: ' + key)
347
+ """)
348
+ encoded = base64.b64encode(keyboard_script.encode()).decode()
349
+ keyboard_cmd = (
350
+ f"cd {repo_root} && "
351
+ f"source .venv_inference/bin/activate && "
352
+ f"python -c \"import base64;exec(base64.b64decode('{encoded}'))\""
353
+ )
354
+
355
+ print("Starting keyboard publisher (pane 2)...")
356
+ _send_to_pane(1, keyboard_cmd, wait=2.0)
357
+
358
+ # --- Pane 3 (bottom-right): Data Exporter (optional) ---
359
+ if config.data_exporter:
360
+ exporter_cmd = (
361
+ f"cd {repo_root} && "
362
+ f"source .venv_data_collection/bin/activate && "
363
+ f"python gear_sonic/scripts/run_data_exporter.py "
364
+ f"--task-prompt '{exporter_prompt}' "
365
+ f"--data-collection-frequency {config.data_exporter_frequency} "
366
+ f"--camera-host {config.camera_host} "
367
+ f"--camera-port {config.camera_port}"
368
+ )
369
+ if config.dataset_name:
370
+ exporter_cmd += f" --dataset-name '{config.dataset_name}'"
371
+
372
+ print("Starting data exporter (pane 3)...")
373
+ _send_to_pane(3, exporter_cmd, wait=2.0)
374
+
375
+ # --- Pane 1 (top-right): VLA Inference ---
376
+ inference_cmd = (
377
+ f"cd {repo_root} && "
378
+ f"source .venv_inference/bin/activate && "
379
+ f"python gear_sonic/scripts/run_vla_inference.py "
380
+ f"--host {config.policy_host} "
381
+ f"--port {config.policy_port} "
382
+ f"--embodiment-tag {config.embodiment_tag} "
383
+ f"--prompt '{config.prompt}' "
384
+ f"--action-publish-rate {config.action_publish_rate} "
385
+ f"--action-horizon {config.action_horizon} "
386
+ f"--camera-host {config.camera_host} "
387
+ f"--camera-port {config.camera_port}"
388
+ )
389
+
390
+ print("Starting VLA inference (pane 1)...")
391
+ _send_to_pane(2, inference_cmd, wait=1.0)
392
+
393
+ # Select the VLA inference pane
394
+ subprocess.run(
395
+ ["tmux", "select-pane", "-t", f"{SESSION_NAME}:0.2"],
396
+ )
397
+
398
+ print()
399
+ print("=" * 60)
400
+ print(" All components launched!")
401
+ print()
402
+ print(f" tmux session: {SESSION_NAME}")
403
+ print()
404
+ if config.sim:
405
+ print(" Window 'sim':")
406
+ print(" MuJoCo Simulator (.venv_sim)")
407
+ print()
408
+ print(" Window 'inference':")
409
+ print(" Pane 0 (top-left): C++ Deploy")
410
+ print(" Pane 1 (bottom-left): Keyboard Publisher")
411
+ print(" Pane 2 (top-right): VLA Inference <-- you are here")
412
+ if config.data_exporter:
413
+ print(" Pane 3 (bottom-right): Data Exporter")
414
+ print()
415
+ print(" ** deploy.sh (pane 0) is waiting for confirmation --")
416
+ print(" click on pane 0 and press Enter to proceed **")
417
+ print()
418
+ print(" Keyboard controls (type in pane 1):")
419
+ print(" p - Pause / resume inference")
420
+ print(" k - Start / stop C++ control loop")
421
+ print(" i - Send initial pose")
422
+ print(" [ - Toggle left hand open/closed (initial pose)")
423
+ print(" ] - Toggle right hand open/closed (initial pose)")
424
+ print(" t <text> - Change inference prompt")
425
+ if config.data_exporter:
426
+ print(" c - Start recording episode")
427
+ print(" s - Stop recording (success)")
428
+ print(" f - Stop recording (failure)")
429
+ print()
430
+ print(" Navigation:")
431
+ print(" Ctrl+b, arrow keys - Switch between panes")
432
+ if config.sim:
433
+ print(" Ctrl+b, n / p - Next / previous window")
434
+ print(" Ctrl+b, d - Detach from session")
435
+ print(" Ctrl+\\ - Kill entire session")
436
+ print("=" * 60)
437
+
438
+ try:
439
+ subprocess.run(["tmux", "attach", "-t", SESSION_NAME])
440
+ except KeyboardInterrupt:
441
+ pass
442
+
443
+ result = subprocess.run(
444
+ ["tmux", "has-session", "-t", SESSION_NAME],
445
+ capture_output=True,
446
+ )
447
+ if result.returncode == 0:
448
+ print(f"\nSession '{SESSION_NAME}' is still running.")
449
+ print(f" Reattach: tmux attach -t {SESSION_NAME}")
450
+ print(f" Kill: tmux kill-session -t {SESSION_NAME}")
451
+
452
+
453
+ def _signal_handler(_sig, _frame):
454
+ print("\nShutdown requested...")
455
+ subprocess.run(
456
+ ["tmux", "kill-session", "-t", SESSION_NAME],
457
+ capture_output=True,
458
+ )
459
+ sys.exit(0)
460
+
461
+
462
+ if __name__ == "__main__":
463
+ signal.signal(signal.SIGINT, _signal_handler)
464
+ config = tyro.cli(InferenceLaunchConfig)
465
+ main(config)
GR00T-WholeBodyControl/gear_sonic/scripts/pico_manager_thread_server.py ADDED
@@ -0,0 +1,2311 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Pico SMPL stream server for body tracking visualization
2
+
3
+ """
4
+
5
+ # Recommended Command Line Arguments:
6
+ # With VR3 PT visualization (by --vis_vr3pt) and optional SMPL body visualization (by --vis_smpl)
7
+ # If you want to enable waist tracking in the VR3 PT visualization, please add --waist_tracking
8
+ python pico_manager_thread_server.py --manager \
9
+ --vis_vr3pt --vis_smpl \
10
+ --waist_tracking
11
+
12
+ # VR3 PT visualization only (without SMPL body) — lower latency
13
+ python pico_manager_thread_server.py --manager --vis_vr3pt
14
+
15
+ # DEBUG VR3 PT VISUALIZATION:
16
+ # A standalone test mode that captures one live frame and visualizes it.
17
+ python pico_manager_thread_server.py --vr3pt_live
18
+
19
+ # TIMING COMPARISON:
20
+ # The visualizer automatically reports timing every 5 seconds when running:
21
+ # [Vis Timing] vr3pt: X.XXms | smpl: X.XXms | render: X.XXms | vr3pt_only: X.XXms | both(vr3pt+smpl): X.XXms
22
+
23
+ """
24
+
25
+ from collections import defaultdict, deque
26
+ from enum import Enum, IntEnum
27
+ import os
28
+ import subprocess
29
+ import threading
30
+ import time
31
+
32
+ import msgpack
33
+ import numpy as np
34
+ from scipy.spatial.transform import Rotation as R, Rotation as sRot
35
+ import torch
36
+ import zmq
37
+
38
+ from gear_sonic.utils.teleop import input_readers
39
+ from gear_sonic.utils.teleop.zmq.zmq_poller import ZMQPoller
40
+ from gear_sonic.trl.utils.rotation_conversion import decompose_rotation_aa
41
+ from gear_sonic.trl.utils.torch_transform import (
42
+ angle_axis_to_quaternion,
43
+ compute_human_joints,
44
+ quat_apply,
45
+ quat_inv,
46
+ quaternion_to_angle_axis,
47
+ quaternion_to_rotation_matrix,
48
+ )
49
+
50
+ try:
51
+ from gear_sonic.utils.teleop.zmq.zmq_planner_sender import (
52
+ build_command_message,
53
+ build_planner_message,
54
+ pack_pose_message,
55
+ )
56
+ except ImportError:
57
+
58
+ def build_command_message(*args, **kwargs) -> bytes:
59
+ raise RuntimeError("build_command_message unavailable")
60
+
61
+ def build_planner_message(*args, **kwargs) -> bytes:
62
+ raise RuntimeError("build_planner_message unavailable")
63
+
64
+ def pack_pose_message(*args, **kwargs) -> bytes:
65
+ raise RuntimeError("pack_pose_message unavailable")
66
+
67
+
68
+ try:
69
+ from gear_sonic.isaac_utils.rotations import remove_smpl_base_rot, smpl_root_ytoz_up
70
+ except ImportError:
71
+ print("Warning: gear_sonic.isaac_utils.rotations not available.")
72
+ remove_smpl_base_rot = None
73
+ smpl_root_ytoz_up = None
74
+
75
+ try:
76
+ import xrobotoolkit_sdk as xrt
77
+ except ImportError:
78
+ xrt = None
79
+
80
+ try:
81
+ from gear_sonic.utils.teleop.solver.hand.g1_gripper_ik_solver import (
82
+ G1GripperInverseKinematicsSolver,
83
+ )
84
+ except ImportError:
85
+ print("Warning: G1GripperInverseKinematicsSolver not available.")
86
+ G1GripperInverseKinematicsSolver = None
87
+
88
+ try:
89
+ from gear_sonic.utils.teleop.vis.vr3pt_pose_visualizer import VR3PtPoseVisualizer
90
+ except ImportError:
91
+ print("Warning: VR3PtPoseVisualizer not available (pyvista may not be installed).")
92
+ VR3PtPoseVisualizer = None
93
+
94
+ try:
95
+ from gear_sonic.utils.teleop.vis.vr3pt_pose_visualizer import get_g1_key_frame_poses
96
+ except ImportError:
97
+ print("Warning: get_g1_key_frame_poses not available (pyvista may not be installed).")
98
+ get_g1_key_frame_poses = None
99
+
100
+
101
+ class LocomotionMode(IntEnum):
102
+ """Locomotion mode enum for robot movement."""
103
+
104
+ IDLE = 0
105
+ SLOW_WALK = 1
106
+ WALK = 2
107
+ RUN = 3
108
+ IDLE_SQUAT = 4
109
+ IDLE_KNEEL_TWO_LEGS = 5
110
+ IDLE_KNEEL = 6
111
+ IDLE_LYING_FACE_DOWN = 7
112
+ CRAWLING = 8
113
+ IDLE_BOXING = 9
114
+ WALK_BOXING = 10
115
+ LEFT_PUNCH = 11
116
+ RIGHT_PUNCH = 12
117
+ RANDOM_PUNCH = 13
118
+ ELBOW_CRAWLING = 14
119
+ LEFT_HOOK = 15
120
+ RIGHT_HOOK = 16
121
+ FORWARD_JUMP = 17
122
+ STEALTH_WALK = 18
123
+ INJURED_WALK = 19
124
+
125
+
126
+ class StreamMode(Enum):
127
+ OFF = 0
128
+ POSE = 1
129
+ PLANNER = 2
130
+ PLANNER_FROZEN_UPPER_BODY = 3
131
+ POSE_PAUSE = 4
132
+ PLANNER_VR_3PT = 5
133
+
134
+
135
+ ### Parse 3 point pose from SMPL
136
+ #
137
+ # OFFSETS: Rotation corrections applied to each keypoint to align SMPL joint frames
138
+ # with the desired robot/visualization coordinate convention.
139
+ #
140
+ # Index mapping (based on [0, 22, 23, 12].index(joint_id)):
141
+ # - OFFSETS[0]: Root/Pelvis (joint 0)
142
+ # - OFFSETS[1]: Left Wrist (joint 22)
143
+ # - OFFSETS[2]: Right Wrist (joint 23)
144
+ # - OFFSETS[3]: Neck (joint 12) - more stable than Head (joint 15) for body tracking
145
+ #
146
+ # Scipy euler rotation convention:
147
+ # - Lowercase "xyz" = EXTRINSIC rotations (about the FIXED/ORIGINAL frame's axes)
148
+ # - Uppercase "XYZ" = INTRINSIC rotations (about the ROTATING body's axes)
149
+ #
150
+ # For EXTRINSIC "xyz" with angles [a, b, c]:
151
+ # All rotations are about the ORIGINAL frame's axes (before any rotation):
152
+ # R_total = R_z(c) @ R_y(b) @ R_x(a) (matrix multiplication order)
153
+ # Applied as: first rotate 'a' about original X, then 'b' about original Y, then 'c' about original Z
154
+ #
155
+ # For INTRINSIC "XYZ" with angles [a, b, c]:
156
+ # Each rotation is about the CURRENT (rotated) frame's axis:
157
+ # R_total = R_x(a) @ R_y(b) @ R_z(c) (matrix multiplication order)
158
+ # Applied as: first rotate 'a' about X, then 'b' about NEW Y, then 'c' about NEW Z
159
+ #
160
+ OFFSETS = [
161
+ sRot.from_euler("xyz", [0, 0, -90], degrees=True), # Root: yaw -90° about fixed Z
162
+ sRot.from_euler("xyz", [90, 0, 0], degrees=True), # L-Wrist: roll +90° about fixed X
163
+ sRot.from_euler(
164
+ "xyz", [-90, 0, 180], degrees=True
165
+ ), # R-Wrist: roll -90° about fixed X, then yaw 180° about fixed Z
166
+ sRot.from_euler("xyz", [0, 0, -90], degrees=True), # Neck: yaw -90° about fixed Z
167
+ ]
168
+
169
+
170
+ def _compute_rel_transform(pose, world_frame, scalar_first=True):
171
+ """
172
+ Transform a pose from Unity coordinate frame to robot coordinate frame.
173
+
174
+ Args:
175
+ pose: np.ndarray shape (7,) - [x, y, z, qx, qy, qz, qw] in Unity frame
176
+ world_frame: np.ndarray shape (7,) - reference frame to compute relative transform
177
+ scalar_first: bool - if True, quaternion is [qw, qx, qy, qz]; if False, [qx, qy, qz, qw]
178
+
179
+ Returns:
180
+ rel_pos: np.ndarray (3,) - position in robot frame
181
+ rel_rot: np.ndarray (4,) - quaternion [qw, qx, qy, qz] in robot frame
182
+
183
+ Coordinate transform matrix Q converts Unity (Y-up, left-handed) to Robot (Z-up, right-handed):
184
+ Unity: X-right, Y-up, Z-forward
185
+ Robot: X-forward, Y-left, Z-up
186
+ """
187
+ world_frame = world_frame.copy()
188
+
189
+ # Q transforms Unity coordinates to Robot coordinates
190
+ # Unity [x, y, z] -> Robot [-x, z, y]
191
+ Q = np.array([[-1, 0, 0], [0, 0, 1], [0, 1, 0.0]])
192
+ pose[:3] = Q @ pose[:3]
193
+ world_frame[:3] = Q @ world_frame[:3]
194
+ rot_base = sRot.from_quat(world_frame[3:], scalar_first=scalar_first).as_matrix()
195
+ rot = sRot.from_quat(pose[3:], scalar_first=scalar_first).as_matrix()
196
+ rel_rot = sRot.from_matrix(Q @ (rot_base.T @ rot) @ Q.T)
197
+ rel_pos = sRot.from_matrix(Q @ rot_base.T @ Q.T).apply(pose[:3] - world_frame[:3])
198
+ return rel_pos, rel_rot.as_quat(scalar_first=True)
199
+
200
+
201
+ def _process_3pt_pose(smpl_pose_np):
202
+ """
203
+ Extract 3-point VR pose (L-Wrist, R-Wrist, Neck) from full SMPL body joint poses.
204
+
205
+ NOTE: We use Neck (joint 12) instead of Head (joint 15) because:
206
+ - Neck is more rigidly coupled to the torso
207
+ - Head has high DoF (looking around) which doesn't reflect body pose
208
+ - Neck provides more stable tracking for upper body orientation
209
+
210
+ Args:
211
+ smpl_pose_np: np.ndarray shape (24, 7) - 24 SMPL joints, each [x, y, z, qx, qy, qz, qw]
212
+ in Unity frame (scalar-last quaternion format)
213
+
214
+ Returns:
215
+ vr_3pt_pose: np.ndarray shape (3, 7) - 3 keypoints in robot frame
216
+ Each row is [x, y, z, qw, qx, qy, qz] (scalar-FIRST quaternion format)
217
+ Row 0: Left Wrist (SMPL joint 22)
218
+ Row 1: Right Wrist (SMPL joint 23)
219
+ Row 2: Neck (SMPL joint 12)
220
+
221
+ IMPORTANT: Positions and orientations are RELATIVE TO ROOT (pelvis).
222
+
223
+ Processing Steps:
224
+ 1. Transform all 24 joints from Unity frame to robot frame
225
+ 2. Extract 4 keypoints: Root(0), L-Wrist(22), R-Wrist(23), Neck(12)
226
+ 3. Apply per-joint rotation OFFSETS to align joint frames
227
+ 4. Make L-Wrist, R-Wrist, Neck relative to Root (both position and orientation)
228
+ 5. Return only the 3 non-root keypoints
229
+
230
+ Note: Position calibration (wrist offsets, neck kinematic chain) is done in
231
+ ThreePointPose.apply_calibration() to ensure consistency with calibrated
232
+ orientations.
233
+ """
234
+
235
+ # Defensive copy: _compute_rel_transform modifies pose[:3] in-place, which would
236
+ # corrupt the caller's array (e.g. PicoReader._latest) and cause wrong results
237
+ # if the same sample is processed more than once.
238
+ smpl_pose_np = smpl_pose_np.copy()
239
+
240
+ # =========================================================================
241
+ # STEP 1: Transform all joints from Unity frame to robot frame
242
+ # =========================================================================
243
+ # Input: smpl_pose_np[i] = [x, y, z, qx, qy, qz, qw] in Unity frame (scalar-last)
244
+ # Output: body_poses[i] = [x, y, z, qw, qx, qy, qz] in robot frame (scalar-first)
245
+ body_poses = np.zeros((smpl_pose_np.shape[0], 7), dtype=np.float32)
246
+ for i in range(smpl_pose_np.shape[0]):
247
+ pos, orn = _compute_rel_transform(
248
+ smpl_pose_np[i], [0, 0, 0, 0, 0, 0, 1], scalar_first=False
249
+ )
250
+ body_poses[i, :3] = pos # Position in robot frame
251
+ body_poses[i, 3:] = orn # Quaternion [qw, qx, qy, qz] in robot frame
252
+
253
+ # =========================================================================
254
+ # STEP 2 & 3: Extract 4 keypoints and apply rotation OFFSETS
255
+ # =========================================================================
256
+ # We only care about these SMPL joint indices:
257
+ # - Joint 0: Root/Pelvis (reference frame)
258
+ # - Joint 22: Left Wrist
259
+ # - Joint 23: Right Wrist
260
+ # - Joint 12: Neck (more stable than Head joint 15)
261
+ #
262
+ # kp_poses maps these to indices 0, 1, 2, 3 respectively
263
+ positions = np.array([[p[0], p[1], p[2]] for p in body_poses])
264
+ kp_poses = np.zeros((4, 7), dtype=np.float32)
265
+
266
+ for i, pose in enumerate(body_poses):
267
+ if i not in [0, 22, 23, 12]:
268
+ continue # Skip joints we don't care about
269
+
270
+ pos = positions[i]
271
+
272
+ # Map SMPL joint index to our keypoint index (0-3)
273
+ # rel_i: 0=Root, 1=L-Wrist, 2=R-Wrist, 3=Neck
274
+ rel_i = [0, 22, 23, 12].index(i)
275
+
276
+ # Extract quaternion and apply rotation offset
277
+ # pose[3:7] is [qw, qx, qy, qz] (scalar-first from _compute_rel_transform)
278
+ quat = np.array([pose[3], pose[4], pose[5], pose[6]])
279
+
280
+ # Apply offset: new_rotation = original_rotation * OFFSET
281
+ # This post-multiplies the offset (intrinsic rotation)
282
+ rot_quat = (sRot.from_quat(quat, scalar_first=True) * OFFSETS[rel_i]).as_quat(
283
+ scalar_first=False
284
+ )
285
+
286
+ kp_poses[rel_i, 3:] = rot_quat # Store as scalar-last temporarily for scipy compatibility
287
+ kp_poses[rel_i, :3] = pos
288
+
289
+ # =========================================================================
290
+ # STEP 4: Make positions and orientations RELATIVE TO ROOT
291
+ # =========================================================================
292
+ # This transforms everything into the root's local coordinate frame.
293
+ # After this step:
294
+ # - Root's position would be (0,0,0) and orientation identity (but we don't return root)
295
+ # - Other keypoints are expressed relative to root
296
+ root_pos = kp_poses[0, :3].copy()
297
+ root_quat = kp_poses[0, 3:].copy() # Still scalar-last for scipy
298
+
299
+ for i in range(1, 4):
300
+ # Position: subtract root position, then rotate by inverse of root orientation
301
+ kp_poses[i, :3] = sRot.from_quat(root_quat).inv().apply(kp_poses[i, :3] - root_pos)
302
+
303
+ # Orientation: compute relative rotation (root_inv * keypoint_rot)
304
+ # Result stored as scalar-FIRST [qw, qx, qy, qz]
305
+ kp_poses[i, 3:] = (
306
+ sRot.from_quat(root_quat).inv() * sRot.from_quat(kp_poses[i, 3:])
307
+ ).as_quat(scalar_first=True)
308
+
309
+ # =========================================================================
310
+ # STEP 5: Return only L-Wrist, R-Wrist, Neck (skip Root)
311
+ # =========================================================================
312
+ # NOTE: Position and orientation calibration (including neck position via kinematic
313
+ # chain) is done in ThreePointPose.apply_calibration() to ensure consistency
314
+ # between calibrated orientation and computed neck position.
315
+ # kp_poses[1:] = indices 1, 2, 3 = L-Wrist, R-Wrist, Neck
316
+ # Each row: [x, y, z, qw, qx, qy, qz] relative to root, scalar-first quaternion
317
+ return kp_poses[1:]
318
+
319
+
320
+ # =============================================================================
321
+ # VR 3-Point Pose Visualization Functions
322
+ # =============================================================================
323
+
324
+
325
+ def run_vr3pt_visualizer_test():
326
+ """
327
+ Standalone test for VR 3-point pose visualizer using PyVista.
328
+ Run this to verify the reference frames are displayed correctly.
329
+ """
330
+ if VR3PtPoseVisualizer is None:
331
+ raise ImportError("VR3PtPoseVisualizer not available. Install pyvista: pip install pyvista")
332
+
333
+ print("=" * 60)
334
+ print("VR 3-Point Pose Visualizer Test (PyVista)")
335
+ print("=" * 60)
336
+ print("\nExpected reference frames (all with RGB axes for XYZ):")
337
+ print(" 1. WHITE ball at origin (0, 0, 0) - World frame")
338
+ print(" 2. CYAN ball at (0, 0, 0.35) - Looking forward (identity)")
339
+ print(" 3. MAGENTA ball at (0, 0.4, 0.25) - Looking left (yaw +90°)")
340
+ print(" 4. YELLOW ball at (0.4, 0, 0.15) - Looking down (pitch +90°)")
341
+ print("\nClose the window to exit.")
342
+ print("=" * 60)
343
+
344
+ visualizer = VR3PtPoseVisualizer(axis_length=0.08, ball_radius=0.015, with_g1_robot=True)
345
+ visualizer.show_static()
346
+
347
+
348
+ def run_vr3pt_live_visualizer():
349
+ """
350
+ Live visualizer for real VR 3-point pose data from Pico.
351
+ Captures one frame from Pico and displays it alongside reference frames.
352
+ """
353
+ if xrt is None:
354
+ raise ImportError(
355
+ "XRoboToolkit SDK not available. Install xrobotoolkit_sdk to use live visualizer."
356
+ )
357
+
358
+ if VR3PtPoseVisualizer is None:
359
+ raise ImportError("VR3PtPoseVisualizer not available. Install pyvista: pip install pyvista")
360
+
361
+ print("=" * 60)
362
+ print("VR 3-Point Pose Live Visualizer (PyVista)")
363
+ print("=" * 60)
364
+
365
+ # Initialize XRT
366
+ subprocess.Popen(["bash", "/opt/apps/roboticsservice/runService.sh"])
367
+ xrt.init()
368
+ print("Waiting for body tracking data...")
369
+ while not xrt.is_body_data_available():
370
+ print("waiting for body data...")
371
+ time.sleep(1)
372
+
373
+ print("Body data available! Capturing VR 3-point pose...")
374
+
375
+ # Capture body poses and compute vr_3pt_pose
376
+ body_poses = xrt.get_body_joints_pose()
377
+ body_poses_np = np.array(body_poses)
378
+
379
+ # Process to get 3-point pose (L-Wrist, R-Wrist, Neck)
380
+ vr_3pt_pose = _process_3pt_pose(body_poses_np)
381
+
382
+ print(f"\nCaptured vr_3pt_pose shape: {vr_3pt_pose.shape}")
383
+ print(f" L-Wrist: pos={vr_3pt_pose[0, :3]}, quat_wxyz={vr_3pt_pose[0, 3:]}")
384
+ print(f" R-Wrist: pos={vr_3pt_pose[1, :3]}, quat_wxyz={vr_3pt_pose[1, 3:]}")
385
+ print(f" Neck: pos={vr_3pt_pose[2, :3]}, quat_wxyz={vr_3pt_pose[2, 3:]}")
386
+
387
+ print("\nDisplaying visualization...")
388
+ print("Close the window to exit.")
389
+ print("=" * 60)
390
+
391
+ visualizer = VR3PtPoseVisualizer(axis_length=0.08, ball_radius=0.015, with_g1_robot=True)
392
+ visualizer.show_with_vr_pose(vr_3pt_pose)
393
+
394
+
395
+ def run_vr3pt_realtime_visualizer(update_hz: int = 10):
396
+ """
397
+ Real-time visualizer for VR 3-point pose data from Pico.
398
+ Continuously updates the visualization with live data.
399
+
400
+ Args:
401
+ update_hz: Update rate in Hz (default 10)
402
+ """
403
+ if xrt is None:
404
+ raise ImportError(
405
+ "XRoboToolkit SDK not available. Install xrobotoolkit_sdk to use realtime visualizer."
406
+ )
407
+
408
+ if VR3PtPoseVisualizer is None:
409
+ raise ImportError("VR3PtPoseVisualizer not available. Install pyvista: pip install pyvista")
410
+
411
+ print("=" * 60)
412
+ print("VR 3-Point Pose Real-time Visualizer (PyVista)")
413
+ print("=" * 60)
414
+
415
+ # Initialize XRT
416
+ subprocess.Popen(["bash", "/opt/apps/roboticsservice/runService.sh"])
417
+ xrt.init()
418
+ print("Waiting for body tracking data...")
419
+ while not xrt.is_body_data_available():
420
+ print("waiting for body data...")
421
+ time.sleep(1)
422
+
423
+ print("Body data available! Starting real-time visualization...")
424
+ print(f"Update rate: {update_hz} Hz")
425
+ print("Close the window or press 'q' to exit.")
426
+ print("=" * 60)
427
+
428
+ # Use the VR3PtPoseVisualizer for real-time visualization with G1 robot
429
+ visualizer = VR3PtPoseVisualizer(axis_length=0.08, ball_radius=0.015, with_g1_robot=True)
430
+ visualizer.create_realtime_plotter(interactive=True)
431
+
432
+ try:
433
+ while visualizer.is_open:
434
+ # Get new data from Pico
435
+ body_poses = xrt.get_body_joints_pose()
436
+ body_poses_np = np.array(body_poses)
437
+ vr_3pt_pose = _process_3pt_pose(body_poses_np)
438
+
439
+ # Update visualization
440
+ visualizer.update_vr_poses(vr_3pt_pose)
441
+ visualizer.render()
442
+
443
+ time.sleep(1.0 / update_hz)
444
+ except KeyboardInterrupt:
445
+ print("\nInterrupted by user")
446
+ finally:
447
+ visualizer.close()
448
+
449
+
450
+ def process_smpl_joints(body_pose, global_orient, transl):
451
+ """Process SMPL parameters to compute local joints.
452
+
453
+ Args:
454
+ body_pose: Body pose tensor, shape (T, 69)
455
+ global_orient: Global orientation tensor, shape (T, 3)
456
+ transl: Translation tensor, shape (T, 3)
457
+
458
+ Returns:
459
+ Dictionary with processed joints and parameters
460
+ """
461
+ # Convert global_orient to quaternion and apply transformations (robust if utils missing)
462
+ global_orient_quat = angle_axis_to_quaternion(global_orient)
463
+ if smpl_root_ytoz_up is not None:
464
+ global_orient_quat = smpl_root_ytoz_up(global_orient_quat)
465
+ global_orient_new = quaternion_to_angle_axis(global_orient_quat)
466
+
467
+ # Compute joints and vertices using SMPL model (single forward pass)
468
+ joints = compute_human_joints(
469
+ body_pose=body_pose[..., :63],
470
+ global_orient=global_orient_new,
471
+ ) # (*, 24, 3)
472
+
473
+ # Apply base rotation removal and compute local joints
474
+ if remove_smpl_base_rot is not None:
475
+ global_orient_quat = remove_smpl_base_rot(global_orient_quat, w_last=False)
476
+
477
+ global_orient_quat_inv = quat_inv(global_orient_quat).unsqueeze(1).repeat(1, joints.shape[1], 1)
478
+ smpl_joints_local = quat_apply(global_orient_quat_inv, joints)
479
+ global_orient_mat = quaternion_to_rotation_matrix(global_orient_quat)
480
+ global_orient_6d = global_orient_mat[..., :2].reshape(1, 6)
481
+
482
+ return {
483
+ "smpl_pose": body_pose,
484
+ "joints": joints,
485
+ "smpl_joints_local": smpl_joints_local,
486
+ "global_orient_quat": global_orient_quat,
487
+ "global_orient_6d": global_orient_6d,
488
+ "adjusted_transl": transl,
489
+ }
490
+
491
+
492
+ def generate_finger_data(hand: str, trigger: float, grip: float) -> np.ndarray:
493
+ """
494
+ Generate finger position data from Pico controller button states.
495
+
496
+ Args:
497
+ hand: "left" or "right"
498
+ trigger: Trigger button value (0-1)
499
+ grip: Grip button value (0-1)
500
+
501
+ Returns:
502
+ Array of shape [25, 4, 4] representing fingertip positions
503
+ """
504
+ fingertips = np.zeros([25, 4, 4])
505
+
506
+ thumb = 0
507
+ middle = 10
508
+ # Control thumb based on shoulder button state (index 4 is thumb tip)
509
+ fingertips[4 + thumb, 0, 3] = 1.0 # open thumb
510
+ if trigger > 0.5:
511
+ fingertips[4 + middle, 0, 3] = 1.0 # close middle
512
+
513
+ return fingertips
514
+
515
+
516
+ # Joystick deadzone threshold
517
+ JOYSTICK_DEADZONE = 0.15
518
+
519
+
520
+ class YawAccumulator:
521
+ """Accumulates yaw heading angle based on joystick input."""
522
+
523
+ def __init__(self, yaw_gain: float = 1.5, deadzone: float = JOYSTICK_DEADZONE):
524
+ self.yaw_gain = yaw_gain
525
+ self.deadzone = deadzone
526
+ self.reset()
527
+
528
+ def reset(self):
529
+ """Reset facing direction to default (1,0,0)."""
530
+ self.heading = [1.0, 0.0, 0.0]
531
+ self.yaw_angle_rad = 0.0
532
+ self.dyaw = 0.0
533
+ print("YawAccumulator: reset yaw angle to 0.0")
534
+
535
+ def yaw_angle(self) -> float:
536
+ """Get current yaw angle in radians."""
537
+ return self.yaw_angle_rad
538
+
539
+ def yaw_angle_change(self) -> float:
540
+ """Get current yaw angle change in radians."""
541
+ return self.dyaw
542
+
543
+ def update(self, rx: float, dt: float) -> list[float]:
544
+ """
545
+ Update facing direction based on right stick x-axis input.
546
+
547
+ Args:
548
+ rx: Right stick x-axis value (-1 to 1)
549
+ dt: Time delta in seconds
550
+
551
+ Returns:
552
+ Facing direction as [x, y, 0.0]
553
+ """
554
+ self.dyaw = self.yaw_gain * (-rx) * dt
555
+ if abs(rx) >= self.deadzone:
556
+ self.yaw_angle_rad += self.dyaw
557
+ self.heading = [np.cos(self.yaw_angle_rad), np.sin(self.yaw_angle_rad), 0.0]
558
+ return self.heading
559
+
560
+
561
+ def compute_from_body_poses(parent_indices: list, device, body_poses_np: np.ndarray):
562
+ """
563
+ Compute local joints and body orientation from provided body_poses_np.
564
+ """
565
+ positions = body_poses_np[:, :3]
566
+ global_quats = body_poses_np[:, [6, 3, 4, 5]]
567
+
568
+ # Convert to local rotations
569
+ global_rots = sRot.from_quat(global_quats, scalar_first=True)
570
+ global_rots = global_rots * sRot.from_euler("y", 180, degrees=True)
571
+
572
+ local_rots = []
573
+ for i in range(24):
574
+ if parent_indices[i] == -1:
575
+ local_rots.append(global_rots[i])
576
+ else:
577
+ local_rot = global_rots[parent_indices[i]].inv() * global_rots[i]
578
+ local_rots.append(local_rot)
579
+
580
+ pose_aa = np.array([rot.as_rotvec() for rot in local_rots])
581
+
582
+ body_pose = torch.from_numpy(pose_aa[1:].flatten()).float().to(device).unsqueeze(0)
583
+ global_orient = torch.from_numpy(pose_aa[0]).float().to(device).unsqueeze(0)
584
+ transl = torch.from_numpy(positions[0]).float().to(device).unsqueeze(0)
585
+
586
+ return process_smpl_joints(body_pose, global_orient, transl)
587
+
588
+
589
+ # def compute_latest_frame(parent_indices: list, device) -> tuple[np.ndarray, np.ndarray]:
590
+ # """
591
+ # Pull body data from XRoboToolkit, compute local SMPL joints and body orientation.
592
+ # Returns (smpl_joints_local_np [24,3], global_orient_quat_np [4,])
593
+ # """
594
+ # body_poses = xrt.get_body_joints_pose()
595
+ # body_poses_np = np.array(body_poses)
596
+ # return compute_from_body_poses(parent_indices, device, body_poses_np)
597
+
598
+
599
+ def init_hand_ik_solvers():
600
+ """Initialize hand IK solvers if available."""
601
+ if G1GripperInverseKinematicsSolver is not None:
602
+ left_solver = G1GripperInverseKinematicsSolver(side="left")
603
+ right_solver = G1GripperInverseKinematicsSolver(side="right")
604
+ print("Hand IK solvers initialized")
605
+ return left_solver, right_solver
606
+ print("Warning: Hand IK solvers not available")
607
+ return None, None
608
+
609
+
610
+ # Readers that expose `get_controller_data()` returning the IsaacTeleop
611
+ # controller_data dict schema (left/right trigger/squeeze, thumbstick, clicks).
612
+ # Tuple form keeps the dispatch sites uniform if/when a second reader speaks
613
+ # the same schema.
614
+ _ISAAC_TELEOP_READERS = (input_readers.IsaacTeleopReader,)
615
+
616
+
617
+ def get_controller_inputs(reader=None):
618
+ """Fetch controller button/trigger states from XRoboToolkit or IsaacTeleop."""
619
+ if isinstance(reader, _ISAAC_TELEOP_READERS):
620
+ ctrl = reader.get_controller_data()
621
+ if ctrl is None:
622
+ return False, 0.0, 0.0, 0.0, 0.0
623
+ return (
624
+ False,
625
+ float(ctrl.get("left_trigger_value", 0.0)),
626
+ float(ctrl.get("right_trigger_value", 0.0)),
627
+ float(ctrl.get("left_squeeze_value", 0.0)),
628
+ float(ctrl.get("right_squeeze_value", 0.0)),
629
+ )
630
+ left_trigger = xrt.get_left_trigger()
631
+ right_trigger = xrt.get_right_trigger()
632
+ left_grip = xrt.get_left_grip()
633
+ right_grip = xrt.get_right_grip()
634
+ left_menu_button = xrt.get_left_menu_button()
635
+ return left_menu_button, left_trigger, right_trigger, left_grip, right_grip
636
+
637
+
638
+ def get_controller_axes(reader=None):
639
+ """Fetch joystick axes (lx, ly, rx, ry). Falls back to zeros if not available."""
640
+ if isinstance(reader, _ISAAC_TELEOP_READERS):
641
+ ctrl = reader.get_controller_data()
642
+ if ctrl is None:
643
+ return 0.0, 0.0, 0.0, 0.0
644
+ left_thumbstick = ctrl.get("left_thumbstick", [0.0, 0.0])
645
+ right_thumbstick = ctrl.get("right_thumbstick", [0.0, 0.0])
646
+ return (
647
+ float(left_thumbstick[0]),
648
+ float(left_thumbstick[1]),
649
+ float(right_thumbstick[0]),
650
+ float(right_thumbstick[1]),
651
+ )
652
+ if xrt is None:
653
+ return 0.0, 0.0, 0.0, 0.0
654
+ try:
655
+ left_axis = xrt.get_left_axis() # expected [x, y]
656
+ right_axis = xrt.get_right_axis() # expected [x, y]
657
+ lx = float(left_axis[0]) if len(left_axis) >= 1 else 0.0
658
+ ly = float(left_axis[1]) if len(left_axis) >= 2 else 0.0
659
+ rx = float(right_axis[0]) if len(right_axis) >= 1 else 0.0
660
+ ry = float(right_axis[1]) if len(right_axis) >= 2 else 0.0
661
+ return lx, ly, rx, ry
662
+ except Exception:
663
+ return 0.0, 0.0, 0.0, 0.0
664
+
665
+
666
+ def get_menu_buttons(reader=None):
667
+ """Fetch both menu buttons (left, right). Falls back to False if not available."""
668
+ if isinstance(reader, _ISAAC_TELEOP_READERS):
669
+ return False, False
670
+ if xrt is None:
671
+ return False, False
672
+
673
+ def _safe_btn(attr):
674
+ try:
675
+ fn = getattr(xrt, attr)
676
+ return bool(fn())
677
+ except Exception:
678
+ return False
679
+
680
+ left = _safe_btn("get_left_menu_button")
681
+ right = _safe_btn("get_right_menu_button")
682
+ return left, right
683
+
684
+
685
+ def get_axis_clicks(reader=None):
686
+ """Fetch both axis click buttons (left, right). Falls back to False if not available."""
687
+ if isinstance(reader, _ISAAC_TELEOP_READERS):
688
+ ctrl = reader.get_controller_data()
689
+ if ctrl is None:
690
+ return False, False
691
+ return (
692
+ float(ctrl.get("left_thumbstick_click", 0.0)) > 0.5,
693
+ float(ctrl.get("right_thumbstick_click", 0.0)) > 0.5,
694
+ )
695
+ if xrt is None:
696
+ return False, False
697
+
698
+ def _safe_btn(attr):
699
+ try:
700
+ fn = getattr(xrt, attr)
701
+ return bool(fn())
702
+ except Exception:
703
+ return False
704
+
705
+ left = _safe_btn("get_left_axis_click")
706
+ right = _safe_btn("get_right_axis_click")
707
+ return left, right
708
+
709
+
710
+ def get_face_buttons(reader=None):
711
+ """Fetch primary face buttons A and X. Returns (a_pressed, x_pressed)."""
712
+ if isinstance(reader, _ISAAC_TELEOP_READERS):
713
+ ctrl = reader.get_controller_data()
714
+ if ctrl is None:
715
+ return False, False
716
+ return (
717
+ float(ctrl.get("right_primary_click", 0.0)) > 0.5,
718
+ float(ctrl.get("left_primary_click", 0.0)) > 0.5,
719
+ )
720
+ if xrt is None:
721
+ return False, False
722
+ try:
723
+ a_pressed = bool(xrt.get_A_button())
724
+ x_pressed = bool(xrt.get_X_button())
725
+ return a_pressed, x_pressed
726
+ except Exception:
727
+ return False, False
728
+
729
+
730
+ def get_abxy_buttons(reader=None):
731
+ """Fetch A,B,X,Y face buttons as booleans (a,b,x,y)."""
732
+ if isinstance(reader, _ISAAC_TELEOP_READERS):
733
+ ctrl = reader.get_controller_data()
734
+ if ctrl is None:
735
+ return False, False, False, False
736
+ return (
737
+ float(ctrl.get("right_primary_click", 0.0)) > 0.5,
738
+ float(ctrl.get("right_secondary_click", 0.0)) > 0.5,
739
+ float(ctrl.get("left_primary_click", 0.0)) > 0.5,
740
+ float(ctrl.get("left_secondary_click", 0.0)) > 0.5,
741
+ )
742
+ if xrt is None:
743
+ return False, False, False, False
744
+ try:
745
+ a_pressed = bool(xrt.get_A_button())
746
+ b_pressed = bool(xrt.get_B_button())
747
+ x_pressed = bool(xrt.get_X_button())
748
+ y_pressed = bool(xrt.get_Y_button())
749
+ return a_pressed, b_pressed, x_pressed, y_pressed
750
+ except Exception:
751
+ return False, False, False, False
752
+
753
+
754
+ def compute_hand_joints_from_inputs(
755
+ left_solver, right_solver, left_trigger, left_grip, right_trigger, right_grip
756
+ ) -> tuple[np.ndarray, np.ndarray]:
757
+ """Compute left/right hand joints using IK solvers, or zeros if unavailable."""
758
+ if left_solver is not None and right_solver is not None:
759
+ left_finger_data = generate_finger_data("left", left_trigger, left_grip)
760
+ right_finger_data = generate_finger_data("right", right_trigger, right_grip)
761
+ left_hand_joints = left_solver({"position": left_finger_data})
762
+ right_hand_joints = right_solver({"position": right_finger_data})
763
+ else:
764
+ left_hand_joints = np.zeros((1, 7), dtype=np.float32)
765
+ right_hand_joints = np.zeros((1, 7), dtype=np.float32)
766
+ return left_hand_joints, right_hand_joints
767
+
768
+
769
+ def _quat_lerp_normalized(q0: np.ndarray, q1: np.ndarray, alpha: float) -> np.ndarray:
770
+ """
771
+ Linear interpolate two quaternions and renormalize. Input shape (4,), xyzw order.
772
+ Ensures shortest path by flipping sign if dot < 0.
773
+ """
774
+ dot = float(np.dot(q0, q1))
775
+ if dot < 0.0:
776
+ q1 = -q1
777
+ q = (1.0 - alpha) * q0 + alpha * q1
778
+ norm = np.linalg.norm(q)
779
+ if norm > 0:
780
+ q = q / norm
781
+ return q
782
+
783
+
784
+ def _interp_pose_axis_angle(
785
+ prev_pose: np.ndarray, curr_pose: np.ndarray, alpha: float
786
+ ) -> np.ndarray:
787
+ """
788
+ Interpolate axis-angle joint poses by converting to quats, lerp-normalize, then back.
789
+ prev_pose, curr_pose: (21,3) axis-angle (rotvec)
790
+ Returns (21,3) axis-angle.
791
+ """
792
+ prev_quats = sRot.from_rotvec(prev_pose.reshape(-1, 3)).as_quat() # (N,4) xyzw
793
+ curr_quats = sRot.from_rotvec(curr_pose.reshape(-1, 3)).as_quat()
794
+ out_quats = np.empty_like(prev_quats)
795
+ for i in range(prev_quats.shape[0]):
796
+ out_quats[i] = _quat_lerp_normalized(prev_quats[i], curr_quats[i], alpha)
797
+ out_pose = sRot.from_quat(out_quats).as_rotvec().reshape(prev_pose.shape)
798
+ return out_pose
799
+
800
+
801
+ class PicoReader:
802
+ """
803
+ Background reader that pulls Pico/XRT data as fast as possible and computes dt/FPS.
804
+ """
805
+
806
+ def __init__(self, max_queue_size: int = 15):
807
+ self._stop = threading.Event()
808
+ self._thread = threading.Thread(target=self._run, daemon=True)
809
+ self._last_t = None
810
+ self._fps_ema = 0.0
811
+ self._last_stamp_ns = None
812
+ self._latest = None
813
+ self._lock = threading.Lock()
814
+
815
+ def start(self):
816
+ self._thread.start()
817
+
818
+ def stop(self):
819
+ self._stop.set()
820
+ self._thread.join(timeout=1.0)
821
+
822
+ def get_latest(self):
823
+ with self._lock:
824
+ return self._latest
825
+
826
+ @property
827
+ def disconnected(self) -> bool:
828
+ return False
829
+
830
+ def clear_disconnect(self):
831
+ pass
832
+
833
+ def get_timestamp_ns(self) -> int:
834
+ if xrt is None:
835
+ return 0
836
+ return int(xrt.get_time_stamp_ns())
837
+
838
+ def _run(self):
839
+ last_report = time.time()
840
+ while not self._stop.is_set():
841
+ if not xrt.is_body_data_available():
842
+ time.sleep(0.001)
843
+ continue
844
+ stamp_ns = xrt.get_time_stamp_ns()
845
+ prev_stamp_ns = self._last_stamp_ns
846
+ if prev_stamp_ns is not None and stamp_ns == prev_stamp_ns:
847
+ time.sleep(0.000001)
848
+ continue
849
+ # Compute device-based dt/fps using timestamp deltas (ns -> s)
850
+ device_dt = ((stamp_ns - prev_stamp_ns) * 1e-9) if prev_stamp_ns is not None else 0.0
851
+ if device_dt > 0.0:
852
+ inst = 1.0 / device_dt
853
+ self._fps_ema = inst if self._fps_ema == 0.0 else (0.9 * self._fps_ema + 0.1 * inst)
854
+ self._last_stamp_ns = stamp_ns
855
+ t_realtime = time.time()
856
+ t_monotonic = time.monotonic()
857
+ try:
858
+ body_poses = xrt.get_body_joints_pose()
859
+
860
+ sample = {
861
+ "body_poses_np": np.array(body_poses),
862
+ "timestamp_realtime": t_realtime,
863
+ "timestamp_monotonic": t_monotonic,
864
+ "timestamp_ns": stamp_ns,
865
+ "dt": device_dt,
866
+ "fps": self._fps_ema,
867
+ }
868
+ with self._lock:
869
+ self._latest = sample
870
+ now = time.time()
871
+ if now - last_report >= 5.0:
872
+ print(
873
+ f"[PicoReader] dt_ts: {device_dt*1000.0:.2f} ms, fps: {self._fps_ema:.2f}"
874
+ )
875
+ last_report = now
876
+ except Exception as e:
877
+ print(f"[PicoReader] read error: {e}")
878
+
879
+
880
+ def _pose_stream_common(
881
+ socket,
882
+ buffer_size: int,
883
+ num_frames_to_send: int,
884
+ target_fps: int,
885
+ use_cuda: bool,
886
+ record_dir: str,
887
+ record_format: str,
888
+ stop_event: threading.Event | None = None,
889
+ log_prefix: str = "PoseLoop",
890
+ enable_vis_vr3pt: bool = False,
891
+ with_g1_robot: bool = True,
892
+ enable_waist_tracking: bool = False,
893
+ enable_smpl_vis: bool = False,
894
+ reader=None,
895
+ ):
896
+ """Shared pose streaming loop used by run_pico."""
897
+ if reader is None:
898
+ if xrt is None:
899
+ raise ImportError(
900
+ "XRoboToolkit SDK not available. Install xrobotoolkit_sdk to run pose streaming."
901
+ )
902
+
903
+ # Create reader and start it
904
+ reader = PicoReader(max_queue_size=buffer_size)
905
+ reader.start()
906
+
907
+ # Create 3-point pose processor with visualization settings
908
+ three_point = ThreePointPose(
909
+ enable_vis_vr3pt=enable_vis_vr3pt,
910
+ with_g1_robot=with_g1_robot,
911
+ enable_waist_tracking=enable_waist_tracking,
912
+ enable_smpl_vis=enable_smpl_vis,
913
+ log_prefix=log_prefix,
914
+ )
915
+
916
+ streamer = PoseStreamer(
917
+ socket=socket,
918
+ reader=reader,
919
+ three_point=three_point,
920
+ num_frames_to_send=num_frames_to_send,
921
+ target_fps=target_fps,
922
+ use_cuda=use_cuda,
923
+ record_dir=record_dir,
924
+ record_format=record_format,
925
+ log_prefix=log_prefix,
926
+ )
927
+
928
+ if stop_event is None:
929
+ stop_event = threading.Event()
930
+
931
+ try:
932
+ while not stop_event.is_set():
933
+ streamer.run_once()
934
+ except KeyboardInterrupt:
935
+ pass
936
+ finally:
937
+ # Cleanup resources
938
+ reader.stop()
939
+ three_point.close()
940
+
941
+
942
+ class ThreePointPose:
943
+ """
944
+ Encapsulates everything around calculating 3-point pose from SMPL input.
945
+
946
+ This includes:
947
+ - Processing SMPL poses to extract 3-point VR pose (L-Wrist, R-Wrist, Neck)
948
+ - Calibration logic to align VR poses with G1 robot
949
+ - Optional visualization of 3-point poses
950
+
951
+ Calibration is done in two steps:
952
+ 1. Neck orientation: Captures initial neck orientation to align subsequent poses as upright
953
+ 2. Wrist positions: Aligns wrist positions to match G1 robot key frame positions
954
+ """
955
+
956
+ # Kinematic chain constants for neck position (matches VR3PtPoseVisualizer)
957
+ TORSO_LINK_OFFSET_Z = 0.05 # meters from root to torso_link
958
+ NECK_LINK_LENGTH = 0.35 # meters from torso_link to neck along neck's local Z
959
+
960
+ def __init__(
961
+ self,
962
+ enable_vis_vr3pt: bool = False,
963
+ with_g1_robot: bool = True,
964
+ enable_waist_tracking: bool = False,
965
+ enable_smpl_vis: bool = False,
966
+ log_prefix: str = "ThreePointPose",
967
+ robot_model=None,
968
+ ):
969
+ """
970
+ Initialize 3-point pose processor.
971
+
972
+ Args:
973
+ enable_vis_vr3pt: Whether to enable VR 3pt pose visualization (requires display)
974
+ with_g1_robot: Whether to include G1 robot in visualization
975
+ enable_waist_tracking: Whether to enable waist tracking in visualization
976
+ enable_smpl_vis: Whether to render SMPL body joints in the VR3pt visualizer
977
+ log_prefix: Prefix for log messages
978
+ robot_model: Optional pre-instantiated RobotModel. If None, will create one.
979
+ Used for FK-based calibration (no display required).
980
+ """
981
+ self.log_prefix = log_prefix
982
+ self.with_g1_robot = with_g1_robot
983
+ self.enable_waist_tracking = enable_waist_tracking
984
+ self.enable_smpl_vis = enable_smpl_vis
985
+
986
+ # Robot model for FK-based calibration (headless, no display required)
987
+ self._robot_model = robot_model
988
+ if self._robot_model is None:
989
+ from gear_sonic.data.robot_model.instantiation.g1 import (
990
+ instantiate_g1_robot_model,
991
+ )
992
+
993
+ self._robot_model = instantiate_g1_robot_model()
994
+ print(f"[{log_prefix}] Robot model loaded for FK calibration")
995
+
996
+ # Optional visualization (requires display + PyVista)
997
+ self.vr3pt_visualizer = None
998
+ if enable_vis_vr3pt:
999
+ if VR3PtPoseVisualizer is None:
1000
+ raise ImportError(
1001
+ "VR3PtPoseVisualizer could not be imported but --vis_vr3pt was requested. "
1002
+ "Ensure pyvista is installed: pip install pyvista"
1003
+ )
1004
+ self.vr3pt_visualizer = VR3PtPoseVisualizer(
1005
+ axis_length=0.08,
1006
+ ball_radius=0.015,
1007
+ with_g1_robot=with_g1_robot,
1008
+ robot_model=self._robot_model,
1009
+ enable_waist_tracking=enable_waist_tracking,
1010
+ enable_smpl_vis=enable_smpl_vis,
1011
+ )
1012
+ self.vr3pt_visualizer.create_realtime_plotter(interactive=True)
1013
+ g1_str = " with G1 robot" if with_g1_robot else ""
1014
+ waist_str = " + waist tracking" if enable_waist_tracking else ""
1015
+ smpl_str = " + SMPL body" if enable_smpl_vis else ""
1016
+ print(f"[{log_prefix}] VR 3pt pose visualization enabled{g1_str}{waist_str}{smpl_str}")
1017
+
1018
+ # Calibration state — triggered explicitly by calibrate_now() or reset_with_measured_q()
1019
+ self._calibration_pending = False
1020
+ self._calibration_neck_quat_inv: np.ndarray | None = None # inv(initial neck quat)
1021
+ self._calibration_lwrist_offset: np.ndarray | None = None # position offset
1022
+ self._calibration_rwrist_offset: np.ndarray | None = None
1023
+ self._calibration_lwrist_rot_offset: sRot | None = None # orientation offset
1024
+ self._calibration_rwrist_rot_offset: sRot | None = None
1025
+ # Override robot q for FK during recalibration (e.g. measured joints for VR 3PT)
1026
+ self._override_robot_q: np.ndarray | None = None
1027
+
1028
+ @property
1029
+ def is_pending(self) -> bool:
1030
+ """Check if calibration is pending."""
1031
+ return self._calibration_pending
1032
+
1033
+ @property
1034
+ def is_calibrated(self) -> bool:
1035
+ """Check if calibration has been captured."""
1036
+ return self._calibration_neck_quat_inv is not None
1037
+
1038
+ def process_smpl_pose(
1039
+ self,
1040
+ smpl_pose_np: np.ndarray,
1041
+ smpl_joints_local: np.ndarray | None = None,
1042
+ ) -> np.ndarray:
1043
+ """
1044
+ Process SMPL pose to extract and calibrate 3-point VR pose.
1045
+
1046
+ Args:
1047
+ smpl_pose_np: np.ndarray shape (24, 7) - 24 SMPL joints
1048
+ smpl_joints_local: Optional np.ndarray shape (24, 3) - SMPL local joint
1049
+ positions for body visualization. If provided and SMPL
1050
+ visualization is enabled, the joint spheres are updated.
1051
+
1052
+ Returns:
1053
+ vr_3pt_pose: np.ndarray shape (3, 7) - Calibrated 3-point pose
1054
+ [L-Wrist, R-Wrist, Neck], each row [x, y, z, qw, qx, qy, qz]
1055
+ """
1056
+ # Extract raw 3-point pose from SMPL
1057
+ vr_3pt_pose_raw = _process_3pt_pose(smpl_pose_np)
1058
+
1059
+ # Capture calibration on first valid frame (or after reset)
1060
+ if self._calibration_pending:
1061
+ self._capture_calibration(vr_3pt_pose_raw)
1062
+
1063
+ # Apply calibration to get the final pose
1064
+ vr_3pt_pose = self._apply_calibration(vr_3pt_pose_raw)
1065
+
1066
+ if self.vr3pt_visualizer is not None:
1067
+ self.vr3pt_visualizer.update_from_vr_pose(vr_3pt_pose, waist_scale=1.0)
1068
+ if smpl_joints_local is not None:
1069
+ self.vr3pt_visualizer.update_smpl_joints(smpl_joints_local)
1070
+ self.vr3pt_visualizer.render()
1071
+
1072
+ return vr_3pt_pose
1073
+
1074
+ def close(self) -> None:
1075
+ """Close and cleanup visualizer resources."""
1076
+ if self.vr3pt_visualizer is not None:
1077
+ try:
1078
+ self.vr3pt_visualizer.close()
1079
+ except Exception as e:
1080
+ print(f"[{self.log_prefix}] Warning: Error closing VR3pt visualizer: {e}")
1081
+
1082
+ def calibrate_now(self, body_poses_np: np.ndarray) -> bool:
1083
+ """Calibrate using current SMPL frame against FK of all-zero body joints.
1084
+ Operator should be in zero-reference pose when calling this."""
1085
+ try:
1086
+ vr_3pt_pose_raw = _process_3pt_pose(body_poses_np)
1087
+ self._override_robot_q = np.zeros(29, dtype=np.float64)
1088
+ self._capture_calibration(vr_3pt_pose_raw)
1089
+ print(f"[{self.log_prefix}] Calibration completed (zero-pose reference)")
1090
+ return True
1091
+ except Exception as e:
1092
+ print(f"[{self.log_prefix}] Calibration failed: {e}")
1093
+ import traceback
1094
+
1095
+ traceback.print_exc()
1096
+ return False
1097
+
1098
+ def _capture_calibration(self, vr_3pt_pose: np.ndarray) -> None:
1099
+ """Capture calibration offsets from vr_3pt_pose against G1 FK reference.
1100
+ If neck calibration already exists (e.g. from calibrate_now), it is preserved
1101
+ to avoid jumps from SMPL noise during recalibration."""
1102
+
1103
+ # Step 1: Neck orientation — only capture if not already set
1104
+ if self._calibration_neck_quat_inv is None:
1105
+ neck_quat_wxyz = vr_3pt_pose[2, 3:].copy()
1106
+ neck_rot = sRot.from_quat(neck_quat_wxyz, scalar_first=True)
1107
+ self._calibration_neck_quat_inv = neck_rot.inv().as_quat(scalar_first=True)
1108
+ calib_inv_rot = sRot.from_quat(self._calibration_neck_quat_inv, scalar_first=True)
1109
+
1110
+ # Step 2: Rotate VR wrist positions/orientations by neck inverse
1111
+ lwrist_pos_corrected = calib_inv_rot.apply(vr_3pt_pose[0, :3].copy())
1112
+ rwrist_pos_corrected = calib_inv_rot.apply(vr_3pt_pose[1, :3].copy())
1113
+ lwrist_rot_corrected = calib_inv_rot * sRot.from_quat(vr_3pt_pose[0, 3:], scalar_first=True)
1114
+ rwrist_rot_corrected = calib_inv_rot * sRot.from_quat(vr_3pt_pose[1, 3:], scalar_first=True)
1115
+
1116
+ # Step 3: Get G1 FK reference poses
1117
+ if self._robot_model is None:
1118
+ raise RuntimeError(
1119
+ "Robot model is required for calibration but was not loaded. "
1120
+ "Ensure the G1 robot model and URDF are available."
1121
+ )
1122
+ if get_g1_key_frame_poses is None:
1123
+ raise RuntimeError(
1124
+ "get_g1_key_frame_poses could not be imported. "
1125
+ "Ensure gear_sonic.utils.teleop.vis.vr3pt_pose_visualizer is available."
1126
+ )
1127
+
1128
+ # Convert 29-DOF override to full model config if needed
1129
+ if self._override_robot_q is not None:
1130
+ robot_q = self._robot_model.get_configuration_from_actuated_joints(
1131
+ body_actuated_joint_values=self._override_robot_q[:29]
1132
+ )
1133
+ else:
1134
+ robot_q = None
1135
+ g1_poses = get_g1_key_frame_poses(self._robot_model, q=robot_q)
1136
+
1137
+ g1_lwrist_pos = g1_poses["left_wrist"]["position"]
1138
+ g1_rwrist_pos = g1_poses["right_wrist"]["position"]
1139
+ g1_lwrist_rot = sRot.from_quat(
1140
+ g1_poses["left_wrist"]["orientation_wxyz"], scalar_first=True
1141
+ )
1142
+ g1_rwrist_rot = sRot.from_quat(
1143
+ g1_poses["right_wrist"]["orientation_wxyz"], scalar_first=True
1144
+ )
1145
+
1146
+ # Compute position offsets: calibrated = neck_corrected - offset
1147
+ self._calibration_lwrist_offset = lwrist_pos_corrected - g1_lwrist_pos
1148
+ self._calibration_rwrist_offset = rwrist_pos_corrected - g1_rwrist_pos
1149
+
1150
+ # Compute orientation offsets: calibrated = rot_offset * neck_corrected
1151
+ self._calibration_lwrist_rot_offset = g1_lwrist_rot * lwrist_rot_corrected.inv()
1152
+ self._calibration_rwrist_rot_offset = g1_rwrist_rot * rwrist_rot_corrected.inv()
1153
+
1154
+ self._calibration_pending = False
1155
+ self._override_robot_q = None
1156
+
1157
+ # Log summary
1158
+ source = "override q" if g1_lwrist_pos.any() else "default/zero"
1159
+ print(
1160
+ f"[{self.log_prefix}] Calibration captured (FK ref: {source}):\n"
1161
+ f" L-Wrist pos offset: [{self._calibration_lwrist_offset[0]:.4f}, "
1162
+ f"{self._calibration_lwrist_offset[1]:.4f}, {self._calibration_lwrist_offset[2]:.4f}]\n"
1163
+ f" R-Wrist pos offset: [{self._calibration_rwrist_offset[0]:.4f}, "
1164
+ f"{self._calibration_rwrist_offset[1]:.4f}, {self._calibration_rwrist_offset[2]:.4f}]"
1165
+ )
1166
+
1167
+ def _apply_calibration(self, vr_3pt_pose: np.ndarray) -> np.ndarray:
1168
+ """Apply stored calibration offsets to raw VR 3-point pose."""
1169
+ if self._calibration_neck_quat_inv is None:
1170
+ return vr_3pt_pose
1171
+
1172
+ calibrated = vr_3pt_pose.copy()
1173
+ calib_inv_rot = sRot.from_quat(self._calibration_neck_quat_inv, scalar_first=True)
1174
+
1175
+ # Neck orientation: calibrated = inv(initial) * current
1176
+ neck_rot = sRot.from_quat(vr_3pt_pose[2, 3:], scalar_first=True)
1177
+ calibrated[2, 3:] = (calib_inv_rot * neck_rot).as_quat(scalar_first=True)
1178
+
1179
+ # Wrist positions: rotate by neck inverse, then subtract offset
1180
+ if self._calibration_lwrist_offset is not None:
1181
+ calibrated[0, :3] = (
1182
+ calib_inv_rot.apply(vr_3pt_pose[0, :3]) - self._calibration_lwrist_offset
1183
+ )
1184
+ if self._calibration_rwrist_offset is not None:
1185
+ calibrated[1, :3] = (
1186
+ calib_inv_rot.apply(vr_3pt_pose[1, :3]) - self._calibration_rwrist_offset
1187
+ )
1188
+
1189
+ # Wrist orientations: rot_offset * (neck_inv * current)
1190
+ if self._calibration_lwrist_rot_offset is not None:
1191
+ lw_corrected = calib_inv_rot * sRot.from_quat(vr_3pt_pose[0, 3:], scalar_first=True)
1192
+ calibrated[0, 3:] = (self._calibration_lwrist_rot_offset * lw_corrected).as_quat(
1193
+ scalar_first=True
1194
+ )
1195
+ if self._calibration_rwrist_rot_offset is not None:
1196
+ rw_corrected = calib_inv_rot * sRot.from_quat(vr_3pt_pose[1, 3:], scalar_first=True)
1197
+ calibrated[1, 3:] = (self._calibration_rwrist_rot_offset * rw_corrected).as_quat(
1198
+ scalar_first=True
1199
+ )
1200
+
1201
+ # Neck position via kinematic chain: root → torso_link (+Z) → neck (along calibrated Z)
1202
+ neck_z = sRot.from_quat(calibrated[2, 3:], scalar_first=True).apply([0, 0, 1])
1203
+ calibrated[2, :3] = (
1204
+ np.array([0, 0, self.TORSO_LINK_OFFSET_Z]) + self.NECK_LINK_LENGTH * neck_z
1205
+ ).astype(np.float32)
1206
+
1207
+ return calibrated
1208
+
1209
+ def _clear_calibration(self):
1210
+ """Clear all calibration state."""
1211
+ self._calibration_neck_quat_inv = None
1212
+ self._calibration_lwrist_offset = None
1213
+ self._calibration_rwrist_offset = None
1214
+ self._calibration_lwrist_rot_offset = None
1215
+ self._calibration_rwrist_rot_offset = None
1216
+ self._override_robot_q = None
1217
+
1218
+ def reset(self) -> None:
1219
+ """Reset calibration. Next process_smpl_pose() call will recalibrate."""
1220
+ self._clear_calibration()
1221
+ self._calibration_pending = True
1222
+ print(f"[{self.log_prefix}] Calibration reset, will re-calibrate on next frame")
1223
+
1224
+ def reset_with_measured_q(self, body_q_measured: np.ndarray) -> None:
1225
+ """Recalibrate wrist offsets using measured robot joints (29 DOFs).
1226
+ Preserves neck calibration to avoid jumps from SMPL noise.
1227
+ Next process_smpl_pose() will recompute wrist offsets against FK of these joints."""
1228
+ # Preserve neck calibration — only clear wrist offsets
1229
+ self._calibration_lwrist_offset = None
1230
+ self._calibration_rwrist_offset = None
1231
+ self._calibration_lwrist_rot_offset = None
1232
+ self._calibration_rwrist_rot_offset = None
1233
+ self._override_robot_q = body_q_measured.copy()
1234
+ self._calibration_pending = True
1235
+ print(f"[{self.log_prefix}] Wrist recalibration pending (neck preserved, measured q)")
1236
+
1237
+
1238
+ class PoseStreamer:
1239
+ """Encapsulates the pose streaming loop state and logic."""
1240
+
1241
+ def __init__(
1242
+ self,
1243
+ socket,
1244
+ reader: "PicoReader | input_readers.IsaacTeleopReader",
1245
+ three_point: ThreePointPose,
1246
+ num_frames_to_send: int,
1247
+ target_fps: int,
1248
+ use_cuda: bool,
1249
+ record_dir: str,
1250
+ record_format: str,
1251
+ log_prefix: str = "PoseLoop",
1252
+ ):
1253
+ self.socket = socket
1254
+ self.reader = reader
1255
+ self.num_frames_to_send = num_frames_to_send
1256
+ self.target_fps = target_fps
1257
+ self.record_dir = record_dir
1258
+ self.log_prefix = log_prefix
1259
+
1260
+ # Injected dependencies
1261
+ self.reader = reader
1262
+ self.three_point = three_point
1263
+
1264
+ self.device = (
1265
+ torch.device("cuda") if use_cuda and torch.cuda.is_available() else torch.device("cpu")
1266
+ )
1267
+
1268
+ if record_dir:
1269
+ os.makedirs(record_dir, exist_ok=True)
1270
+ self.record_idx = 0
1271
+
1272
+ self.left_hand_ik_solver, self.right_hand_ik_solver = init_hand_ik_solvers()
1273
+ self.parent_indices = [
1274
+ -1,
1275
+ 0,
1276
+ 0,
1277
+ 0,
1278
+ 1,
1279
+ 2,
1280
+ 3,
1281
+ 4,
1282
+ 5,
1283
+ 6,
1284
+ 7,
1285
+ 8,
1286
+ 9,
1287
+ 9,
1288
+ 9,
1289
+ 12,
1290
+ 13,
1291
+ 14,
1292
+ 16,
1293
+ 17,
1294
+ 18,
1295
+ 19,
1296
+ 20,
1297
+ 22,
1298
+ 23,
1299
+ ][:24]
1300
+
1301
+ self.step = 0
1302
+ self.last_fps_report = time.time()
1303
+ self.fps_counter = 0
1304
+ # NOTE: Sleep budget set to 95% of the ideal frame period so that the actual
1305
+ # FPS lands closer to target_fps despite per-frame processing overhead.
1306
+ self.frame_time = 0.95 / max(1, target_fps)
1307
+ self.frame_buffer = defaultdict(lambda: deque(maxlen=num_frames_to_send))
1308
+
1309
+ self.prev_stamp_ns = None
1310
+ self.prev_smpl_pose_np = None
1311
+ self.prev_smpl_joints_np = None
1312
+ self.prev_body_quat_np = None
1313
+ self.next_target_ns = None
1314
+ self.frame_start = time.time()
1315
+
1316
+ # Data collection button state tracking (edge-triggered)
1317
+ self.toggle_data_collection_last = False
1318
+ self.toggle_data_abort_last = False
1319
+
1320
+ self.buffer_cleared = (
1321
+ True # Start with buffer cleared - wait for full buffer before first send
1322
+ )
1323
+ self.yaw_accumulator = YawAccumulator()
1324
+
1325
+ def reset_yaw(self):
1326
+ """Called when entering pose mode. Resets yaw only.
1327
+ Calibration is triggered separately by the operator (A+B+X+Y → calibrate_now)."""
1328
+ self.yaw_accumulator.reset()
1329
+
1330
+ def on_mode_exit(self):
1331
+ self.frame_buffer.clear()
1332
+ self.prev_stamp_ns = None
1333
+ self.prev_smpl_pose_np = None
1334
+ self.prev_smpl_joints_np = None
1335
+ self.prev_body_quat_np = None
1336
+ self.next_target_ns = None
1337
+ self.buffer_cleared = True
1338
+ self.step = 0
1339
+
1340
+ def run_once(self):
1341
+ """Execute one iteration of the pose streaming loop."""
1342
+ sample = self.reader.get_latest()
1343
+
1344
+ if sample is None:
1345
+ time.sleep(0.005)
1346
+ return
1347
+
1348
+ latest_data = compute_from_body_poses(
1349
+ self.parent_indices, self.device, sample["body_poses_np"]
1350
+ )
1351
+ left_menu_button, left_trigger, right_trigger, left_grip, right_grip = get_controller_inputs(
1352
+ self.reader
1353
+ )
1354
+ # Get A and B button states for data collection control
1355
+ a_pressed, b_pressed, x_pressed, y_pressed = get_abxy_buttons(self.reader)
1356
+
1357
+ # Data collection toggle logic (edge-triggered)
1358
+ # Left grip + A = toggle_data_collection
1359
+ # Left grip + B = toggle_data_abort
1360
+ toggle_data_collection_tmp = a_pressed and left_grip > 0.5
1361
+ toggle_data_abort_tmp = b_pressed and left_grip > 0.5
1362
+
1363
+ # Detect rising edge
1364
+ toggle_data_collection = toggle_data_collection_tmp and not self.toggle_data_collection_last
1365
+ toggle_data_abort = toggle_data_abort_tmp and not self.toggle_data_abort_last
1366
+ self.toggle_data_collection_last = toggle_data_collection_tmp
1367
+ self.toggle_data_abort_last = toggle_data_abort_tmp
1368
+
1369
+ left_hand_joints, right_hand_joints = compute_hand_joints_from_inputs(
1370
+ self.left_hand_ik_solver,
1371
+ self.right_hand_ik_solver,
1372
+ left_trigger,
1373
+ left_grip,
1374
+ right_trigger,
1375
+ right_grip,
1376
+ )
1377
+ smpl_pose_np = (
1378
+ latest_data["smpl_pose"].detach().cpu().numpy()[:, :63].reshape(-1, 21, 3)[0]
1379
+ ).astype(np.float32)
1380
+ smpl_joints_np = (
1381
+ latest_data["smpl_joints_local"].detach().cpu().numpy()[0].astype(np.float32)
1382
+ )
1383
+ body_quat_np = (
1384
+ latest_data["global_orient_quat"].detach().cpu().numpy()[0].astype(np.float32)
1385
+ )
1386
+ curr_stamp_ns = int(sample.get("timestamp_ns", 0))
1387
+ step_ns = int(1e9 / max(1, self.target_fps))
1388
+ if self.prev_stamp_ns is None:
1389
+ self.prev_stamp_ns = curr_stamp_ns
1390
+ self.prev_smpl_pose_np = smpl_pose_np
1391
+ self.prev_smpl_joints_np = smpl_joints_np
1392
+ self.prev_body_quat_np = body_quat_np
1393
+ self.next_target_ns = curr_stamp_ns
1394
+ return
1395
+ if curr_stamp_ns <= self.prev_stamp_ns:
1396
+ return
1397
+ if self.next_target_ns is None:
1398
+ self.next_target_ns = self.prev_stamp_ns + step_ns
1399
+ if self.next_target_ns < self.prev_stamp_ns:
1400
+ self.next_target_ns = self.prev_stamp_ns
1401
+ if self.next_target_ns > curr_stamp_ns:
1402
+ return
1403
+ denom = float(curr_stamp_ns - self.prev_stamp_ns)
1404
+ alpha = float(self.next_target_ns - self.prev_stamp_ns) / denom if denom > 0.0 else 1.0
1405
+ if alpha < 0.0:
1406
+ alpha = 0.0
1407
+ elif alpha > 1.0:
1408
+ alpha = 1.0
1409
+ use_joints = (1.0 - alpha) * self.prev_smpl_joints_np + alpha * smpl_joints_np
1410
+ use_pose = _interp_pose_axis_angle(self.prev_smpl_pose_np, smpl_pose_np, alpha).astype(
1411
+ np.float32
1412
+ )
1413
+ use_body_quat = _quat_lerp_normalized(self.prev_body_quat_np, body_quat_np, alpha).astype(
1414
+ np.float32
1415
+ )
1416
+ N = len(self.frame_buffer["frame_index"])
1417
+
1418
+ ##### From @Jiefeng for directly setting the joint position ######
1419
+ joint_pos = np.zeros(29)
1420
+ body_pose = use_pose.reshape(-1, 21, 3)
1421
+
1422
+ SMPL_L_ELBOW_IDX = 17
1423
+ SMPL_L_WRIST_IDX = 19
1424
+ SMPL_R_ELBOW_IDX = 18
1425
+ SMPL_R_WRIST_IDX = 20
1426
+
1427
+ # G1_L_ELBOW_IDX = 0
1428
+ G1_L_WRIST_ROLL_IDX = 23
1429
+ G1_L_WRIST_PITCH_IDX = 25
1430
+ G1_L_WRIST_YAW_IDX = 27
1431
+
1432
+ # G1_R_ELBOW_IDX = 0
1433
+ G1_R_WRIST_ROLL_IDX = 24 # Done
1434
+ G1_R_WRIST_PITCH_IDX = 26
1435
+ G1_R_WRIST_YAW_IDX = 28
1436
+ smpl_l_elbow_aa = body_pose[:, SMPL_L_ELBOW_IDX]
1437
+ smpl_l_wrist_aa = body_pose[:, SMPL_L_WRIST_IDX]
1438
+ smpl_r_elbow_aa = body_pose[:, SMPL_R_ELBOW_IDX]
1439
+ smpl_r_wrist_aa = body_pose[:, SMPL_R_WRIST_IDX]
1440
+
1441
+ g1_l_elbow_axis = np.array([0, 1, 0])
1442
+ g1_l_elbow_q_twist, g1_l_elbow_q_swing = decompose_rotation_aa(
1443
+ smpl_l_elbow_aa, g1_l_elbow_axis
1444
+ )
1445
+
1446
+ g1_r_elbow_axis = np.array([0, 1, 0])
1447
+ g1_r_elbow_q_twist, g1_r_elbow_q_swing = decompose_rotation_aa(
1448
+ smpl_r_elbow_aa, g1_r_elbow_axis
1449
+ )
1450
+
1451
+ # Move elbow roll/yaw into wrist while preserving wrist pitch from SMPL
1452
+ l_elbow_swing_euler = R.from_quat(g1_l_elbow_q_swing[:, [1, 2, 3, 0]]).as_euler(
1453
+ "XYZ", degrees=False
1454
+ )
1455
+ r_elbow_swing_euler = R.from_quat(g1_r_elbow_q_swing[:, [1, 2, 3, 0]]).as_euler(
1456
+ "XYZ", degrees=False
1457
+ )
1458
+
1459
+ l_wrist_euler = R.from_rotvec(smpl_l_wrist_aa).as_euler("XYZ", degrees=False)
1460
+ r_wrist_euler = R.from_rotvec(smpl_r_wrist_aa).as_euler("XYZ", degrees=False)
1461
+
1462
+ g1_l_wrist_roll = l_elbow_swing_euler[:, 0] + l_wrist_euler[:, 0]
1463
+ g1_l_wrist_pitch = -l_wrist_euler[:, 1]
1464
+ g1_l_wrist_yaw = l_elbow_swing_euler[:, 2] + l_wrist_euler[:, 2]
1465
+
1466
+ g1_r_wrist_roll = -(r_elbow_swing_euler[:, 0] + r_wrist_euler[:, 0])
1467
+ g1_r_wrist_pitch = -r_wrist_euler[:, 1]
1468
+ g1_r_wrist_yaw = r_elbow_swing_euler[:, 2] + r_wrist_euler[:, 2]
1469
+
1470
+ joint_pos[G1_L_WRIST_ROLL_IDX] = g1_l_wrist_roll[0]
1471
+ joint_pos[G1_L_WRIST_PITCH_IDX] = -g1_l_wrist_pitch[0]
1472
+ joint_pos[G1_L_WRIST_YAW_IDX] = g1_l_wrist_yaw[0]
1473
+
1474
+ joint_pos[G1_R_WRIST_ROLL_IDX] = g1_r_wrist_roll[0]
1475
+ joint_pos[G1_R_WRIST_PITCH_IDX] = g1_r_wrist_pitch[0]
1476
+ joint_pos[G1_R_WRIST_YAW_IDX] = g1_r_wrist_yaw[0]
1477
+
1478
+ # Process SMPL pose to get calibrated 3-point VR pose and update visualization
1479
+ # Pass SMPL local joints for optional body visualization in the VR3Pt viewer
1480
+ smpl_joints_for_vis = (
1481
+ latest_data["smpl_joints_local"].detach().cpu().numpy()[0]
1482
+ if self.three_point.enable_smpl_vis
1483
+ else None
1484
+ )
1485
+ vr_3pt_pose = self.three_point.process_smpl_pose(
1486
+ sample["body_poses_np"], smpl_joints_local=smpl_joints_for_vis
1487
+ )
1488
+ ##### From @Jiefeng for directly setting the joint position ######
1489
+
1490
+ self.frame_buffer["smpl_pose"].append(use_pose)
1491
+ self.frame_buffer["smpl_joints"].append(use_joints)
1492
+ self.frame_buffer["body_quat_w"].append(use_body_quat)
1493
+ self.frame_buffer["frame_index"].append(int(self.step))
1494
+ self.frame_buffer["joint_pos"].append(joint_pos)
1495
+ pico_dt = float(sample.get("dt", 0.0))
1496
+ pico_fps = float(sample.get("fps", 0.0))
1497
+ N = len(self.frame_buffer["frame_index"])
1498
+
1499
+ # Wait for buffer to be completely filled before sending first message after clearing
1500
+ buffer_is_full = len(self.frame_buffer["frame_index"]) >= self.num_frames_to_send
1501
+ if buffer_is_full and self.buffer_cleared:
1502
+ # Buffer is now full with fresh data, can start sending
1503
+ self.buffer_cleared = False
1504
+
1505
+ # Get joystick axes for yaw accumulation
1506
+ _, _, rx, _ = get_controller_axes(self.reader)
1507
+ self.yaw_accumulator.update(rx, self.frame_time)
1508
+
1509
+ # Only send if buffer is full and we're not waiting for fresh data
1510
+ if buffer_is_full and not self.buffer_cleared:
1511
+ numpy_data = {
1512
+ "smpl_pose": np.stack((self.frame_buffer["smpl_pose"]), axis=0),
1513
+ "smpl_joints": np.stack((self.frame_buffer["smpl_joints"]), axis=0),
1514
+ "body_quat_w": np.stack((self.frame_buffer["body_quat_w"]), axis=0),
1515
+ "joint_pos": np.stack((self.frame_buffer["joint_pos"]), axis=0),
1516
+ "joint_vel": np.zeros((N, 29)),
1517
+ "vr_position": vr_3pt_pose[:, :3].flatten(),
1518
+ "vr_orientation": vr_3pt_pose[:, 3:].flatten(),
1519
+ "frame_index": np.array((self.frame_buffer["frame_index"]), dtype=np.int64),
1520
+ "left_trigger": np.array([left_trigger], dtype=np.float32),
1521
+ "right_trigger": np.array([right_trigger], dtype=np.float32),
1522
+ "left_grip": np.array([left_grip], dtype=np.float32),
1523
+ "right_grip": np.array([right_grip], dtype=np.float32),
1524
+ "pico_dt": np.array([pico_dt], dtype=np.float32),
1525
+ "pico_fps": np.array([pico_fps], dtype=np.float32),
1526
+ "timestamp_realtime": np.array(
1527
+ [sample.get("timestamp_realtime", 0.0)], dtype=np.float64
1528
+ ),
1529
+ "timestamp_monotonic": np.array(
1530
+ [sample.get("timestamp_monotonic", 0.0)], dtype=np.float64
1531
+ ),
1532
+ "left_hand_joints": left_hand_joints.reshape(-1).astype(np.float32),
1533
+ "right_hand_joints": right_hand_joints.reshape(-1).astype(np.float32),
1534
+ "toggle_data_collection": np.array([toggle_data_collection], dtype=bool),
1535
+ "toggle_data_abort": np.array([toggle_data_abort], dtype=bool),
1536
+ "heading_increment": np.array(
1537
+ [self.yaw_accumulator.yaw_angle_change()], dtype=np.float32
1538
+ ),
1539
+ }
1540
+
1541
+ packed_message = pack_pose_message(numpy_data, topic="pose")
1542
+ self.socket.send(packed_message)
1543
+
1544
+ if self.record_dir:
1545
+ out_path = os.path.join(self.record_dir, f"pose_{self.record_idx:06d}.npz")
1546
+ np.savez_compressed(out_path, **numpy_data)
1547
+ self.record_idx += 1
1548
+
1549
+ self.step += 1
1550
+ self.next_target_ns += step_ns
1551
+ self.prev_stamp_ns = curr_stamp_ns
1552
+ self.prev_smpl_pose_np = smpl_pose_np
1553
+ self.prev_smpl_joints_np = smpl_joints_np
1554
+ self.prev_body_quat_np = body_quat_np
1555
+ self.fps_counter += 1
1556
+ current_time = time.time()
1557
+ if current_time - self.last_fps_report >= 5.0:
1558
+ fps = self.fps_counter / (current_time - self.last_fps_report)
1559
+ print(f"[{self.log_prefix}] FPS: {fps:.2f}, Step: {self.step}")
1560
+ self.fps_counter = 0
1561
+ self.last_fps_report = current_time
1562
+ elapsed = time.time() - self.frame_start
1563
+ if elapsed < self.frame_time:
1564
+ time.sleep(self.frame_time - elapsed)
1565
+ self.frame_start = time.time()
1566
+
1567
+
1568
+ def _init_input_source(
1569
+ input_source: str,
1570
+ buffer_size: int,
1571
+ ) -> "PicoReader | input_readers.IsaacTeleopReader":
1572
+ """Create, start, and wait for readiness of the requested teleop input source."""
1573
+ if input_source == "isaac-teleop":
1574
+ reader = input_readers.IsaacTeleopReader(max_queue_size=buffer_size)
1575
+ reader.start()
1576
+ print("Using Isaac Teleop (in-process CloudXR / DeviceIO), waiting for data...")
1577
+ while reader.get_latest() is None:
1578
+ print("waiting for Isaac Teleop body data (connect the headset to CloudXR)...")
1579
+ time.sleep(1)
1580
+ return reader
1581
+
1582
+ if xrt is None:
1583
+ raise ImportError(
1584
+ "XRoboToolkit SDK not available. Install xrobotoolkit_sdk to run Pico streaming."
1585
+ )
1586
+
1587
+ subprocess.Popen(["bash", "/opt/apps/roboticsservice/runService.sh"])
1588
+ xrt.init()
1589
+ print("Waiting for body tracking data...")
1590
+ while not xrt.is_body_data_available():
1591
+ print("waiting for body data...")
1592
+ time.sleep(1)
1593
+
1594
+ reader = PicoReader(max_queue_size=buffer_size)
1595
+ reader.start()
1596
+ return reader
1597
+
1598
+
1599
+ def run_pico(
1600
+ buffer_size: int = 15,
1601
+ port: int = 5556,
1602
+ num_frames_to_send: int = 5,
1603
+ target_fps: int = 50,
1604
+ use_cuda: bool = False,
1605
+ record_dir: str = "",
1606
+ record_format: str = "npz",
1607
+ enable_vis_vr3pt: bool = False,
1608
+ with_g1_robot: bool = True,
1609
+ enable_waist_tracking: bool = False,
1610
+ enable_smpl_vis: bool = False,
1611
+ input_source: str = "xrt",
1612
+ ):
1613
+ """Run body tracking with real-time visualization and ZMQ streaming."""
1614
+ reader = _init_input_source(input_source, buffer_size)
1615
+ context = zmq.Context()
1616
+ socket = context.socket(zmq.PUB)
1617
+ socket.bind(f"tcp://*:{port}")
1618
+ time.sleep(0.1)
1619
+ print(f"ZMQ socket bound to port {port}")
1620
+ if build_command_message is not None and build_planner_message is not None:
1621
+ try:
1622
+ socket.send(build_command_message(start=False, stop=False, planner=False))
1623
+ socket.send(build_planner_message(0, [0.0, 0.0, 0.0], [1.0, 0.0, 0.0], -1.0, -1.0))
1624
+ except Exception as e:
1625
+ print(f"Warning: failed to send initial command/planner messages: {e}")
1626
+ try:
1627
+ _pose_stream_common(
1628
+ socket=socket,
1629
+ buffer_size=buffer_size,
1630
+ num_frames_to_send=num_frames_to_send,
1631
+ target_fps=target_fps,
1632
+ use_cuda=use_cuda,
1633
+ record_dir=record_dir,
1634
+ record_format=record_format,
1635
+ stop_event=None,
1636
+ log_prefix="Main",
1637
+ enable_vis_vr3pt=enable_vis_vr3pt,
1638
+ with_g1_robot=with_g1_robot,
1639
+ enable_waist_tracking=enable_waist_tracking,
1640
+ enable_smpl_vis=enable_smpl_vis,
1641
+ reader=reader,
1642
+ )
1643
+ finally:
1644
+ socket.close()
1645
+ context.term()
1646
+ print("Threads stopped, ZMQ socket closed")
1647
+
1648
+
1649
+ class FeedbackReader:
1650
+ """Reads feedback from robot via ZMQ and processes measured upper body position to use as frozen targets."""
1651
+
1652
+ def __init__(self, zmq_feedback_host: str = "localhost", zmq_feedback_port: int = 5557):
1653
+ self.poller = ZMQPoller(host=zmq_feedback_host, port=zmq_feedback_port, topic="g1_debug")
1654
+
1655
+ self.upper_body_joint_indices = self._get_upper_body_joint_indices()
1656
+
1657
+ self.upper_body_position_target = None
1658
+ self.left_hand_position_target = None
1659
+ self.right_hand_position_target = None
1660
+ # Full body joint configuration (29 DOFs) as measured from robot,
1661
+ # used for FK when recalibrating VR 3PT tracking against actual robot pose
1662
+ self.full_body_q_measured: np.ndarray | None = None
1663
+
1664
+ def _get_upper_body_joint_indices(self) -> list[int]:
1665
+ # TODO: get from robot model, not hardcoded
1666
+ # robot_model = instantiate_g1_robot_model()
1667
+ # return robot_model.get_joint_group_indices("upper_body")
1668
+ return [12, 13, 14, 15, 22, 16, 23, 17, 24, 18, 25, 19, 26, 20, 27, 21, 28]
1669
+
1670
+ def poll_feedback(self):
1671
+ """Poll for feedback once, and update internal state."""
1672
+ (
1673
+ self.upper_body_position_target,
1674
+ self.left_hand_position_target,
1675
+ self.right_hand_position_target,
1676
+ self.full_body_q_measured,
1677
+ ) = self._process_upper_body_position_targets()
1678
+ print("[PlannerLoop] Saved upper body position target:", self.upper_body_position_target)
1679
+
1680
+ def _process_upper_body_position_targets(
1681
+ self,
1682
+ ) -> tuple[np.ndarray | None, np.ndarray | None, np.ndarray | None, np.ndarray | None]:
1683
+ data = self.poller.get_data()
1684
+
1685
+ if data is None:
1686
+ print("[PlannerLoop] No feedback data received")
1687
+ return None, None, None, None
1688
+
1689
+ unpacked = msgpack.unpackb(data, raw=False)
1690
+ full_body_q = None
1691
+ if "body_q_measured" in unpacked:
1692
+ body_q_swizzled = unpacked["body_q_measured"]
1693
+ full_body_q = np.array(body_q_swizzled, dtype=np.float64)
1694
+ body_q = [body_q_swizzled[i] for i in self.upper_body_joint_indices]
1695
+ else:
1696
+ print("[PlannerLoop] body_q_measured not in feedback data")
1697
+ body_q = None
1698
+
1699
+ if "left_hand_q_measured" in unpacked:
1700
+ left_hand_q = unpacked["left_hand_q_measured"]
1701
+ else:
1702
+ print("[PlannerLoop] left_hand_q_measured not in feedback data")
1703
+ left_hand_q = None
1704
+
1705
+ if "right_hand_q_measured" in unpacked:
1706
+ right_hand_q = unpacked["right_hand_q_measured"]
1707
+ else:
1708
+ print("[PlannerLoop] right_hand_q_measured not in feedback data")
1709
+ right_hand_q = None
1710
+
1711
+ return body_q, left_hand_q, right_hand_q, full_body_q
1712
+
1713
+
1714
+ class PlannerStreamer:
1715
+ """Encapsulates the planner control loop state and logic."""
1716
+
1717
+ def __init__(
1718
+ self,
1719
+ socket,
1720
+ reader: "PicoReader | input_readers.IsaacTeleopReader",
1721
+ three_point: ThreePointPose,
1722
+ poll_hz: int = 20,
1723
+ zmq_feedback_host: str = "localhost",
1724
+ zmq_feedback_port: int = 5557,
1725
+ ):
1726
+ self.socket = socket
1727
+ self.reader = reader
1728
+ self.three_point = three_point
1729
+ self.feedback_reader = FeedbackReader(
1730
+ zmq_feedback_host=zmq_feedback_host, zmq_feedback_port=zmq_feedback_port
1731
+ )
1732
+
1733
+ self.dt = 1.0 / max(1, poll_hz)
1734
+ # Current locomotion mode, default IDLE
1735
+ self.mode = LocomotionMode.IDLE
1736
+ self.prev_ab = False
1737
+ self.prev_xy = False
1738
+ # Persistent facing buffer (unit vector on XY plane)
1739
+ self.yaw_accumulator = YawAccumulator()
1740
+ self.last_send = time.time()
1741
+ self.last_xrt_timestamp = None
1742
+
1743
+ # Hand IK solvers for trigger-controlled hand open/close in VR 3PT mode
1744
+ self.left_hand_ik_solver, self.right_hand_ik_solver = init_hand_ik_solvers()
1745
+
1746
+ def reset_yaw(self):
1747
+ """Called when entering planner mode. Resets state for fresh start."""
1748
+ self.yaw_accumulator.reset()
1749
+
1750
+ def save_upper_body_position_target(self):
1751
+ """Poll feedback and save upper body position target."""
1752
+ self.feedback_reader.poll_feedback()
1753
+
1754
+ def recalibrate_for_vr3pt(self):
1755
+ """
1756
+ Recalibrate VR 3-point pose tracking using the robot's current measured joints.
1757
+
1758
+ Polls the g1_debug feedback to get the robot's actual joint state, then
1759
+ schedules recalibration so VR tracking aligns with the robot's current pose.
1760
+ This prevents sudden jumps when entering VR 3PT mode from PLANNER mode.
1761
+ """
1762
+ self.feedback_reader.poll_feedback()
1763
+ if self.feedback_reader.full_body_q_measured is not None:
1764
+ self.three_point.reset_with_measured_q(self.feedback_reader.full_body_q_measured)
1765
+ print("[PlannerLoop] VR 3PT recalibration scheduled with measured robot pose")
1766
+ else:
1767
+ # Fallback: use zeros if no feedback available
1768
+ print(
1769
+ "[PlannerLoop] WARNING: No feedback data for VR 3PT recalibration, "
1770
+ "using zero body_q as fallback"
1771
+ )
1772
+ self.three_point.reset_with_measured_q(np.zeros(29, dtype=np.float64))
1773
+
1774
+ def run_once(self, stream_mode: StreamMode):
1775
+ """Execute one iteration of the planner control loop."""
1776
+ try:
1777
+ # Avoid sending old commands if XRT timestamp hasn't advanced, in case of headset disconnect
1778
+ xrt_timestamp = self.reader.get_timestamp_ns()
1779
+ if xrt_timestamp == self.last_xrt_timestamp:
1780
+ return
1781
+ self.last_xrt_timestamp = xrt_timestamp
1782
+
1783
+ # A+B => next mode; X+Y => previous mode (rising edges)
1784
+ a_pressed, b_pressed, x_pressed, y_pressed = get_abxy_buttons(self.reader)
1785
+ ab_now = bool(a_pressed) and bool(b_pressed)
1786
+ xy_now = bool(x_pressed) and bool(y_pressed)
1787
+ if ab_now and not self.prev_ab:
1788
+ self.mode = LocomotionMode(min(LocomotionMode.INJURED_WALK, self.mode + 1))
1789
+ print(f"[PlannerLoop] Mode -> {self.mode.value}: {self.mode.name}")
1790
+ if xy_now and not self.prev_xy:
1791
+ self.mode = LocomotionMode(max(LocomotionMode.IDLE, self.mode - 1))
1792
+ print(f"[PlannerLoop] Mode -> {self.mode.value}: {self.mode.name}")
1793
+ self.prev_ab = ab_now
1794
+ self.prev_xy = xy_now
1795
+
1796
+ # Read axes/joysticks to control movement, facing, speed and mode
1797
+ lx, ly, rx, ry = get_controller_axes(self.reader)
1798
+
1799
+ # Facing from RIGHT stick: continuous yaw based on rx (right = turn right, left = turn left)
1800
+ facing = self.yaw_accumulator.update(rx, self.dt)
1801
+
1802
+ raw_mag = np.hypot(lx, ly)
1803
+ raw_mag = np.clip(raw_mag, 0.0, 1.0)
1804
+ if np.abs(raw_mag) < JOYSTICK_DEADZONE:
1805
+ mag = 0.0
1806
+ speed = -1.0
1807
+ mode_to_send = LocomotionMode.IDLE
1808
+ else:
1809
+ mag = (raw_mag - JOYSTICK_DEADZONE) / (1.0 - JOYSTICK_DEADZONE)
1810
+ if mag > 1.0:
1811
+ mag = 1.0
1812
+ mode_to_send = self.mode
1813
+
1814
+ if self.mode == LocomotionMode.SLOW_WALK:
1815
+ speed = 0.1 + 0.5 * mag # 0.1 .. 0.6
1816
+ elif self.mode == LocomotionMode.WALK:
1817
+ speed = -1.0
1818
+ elif self.mode == LocomotionMode.RUN:
1819
+ speed = 1.5 + 3 * mag # 1.5 .. 4.5
1820
+ else:
1821
+ speed = mag # default 0 .. 1.0
1822
+
1823
+ denom = raw_mag if raw_mag > 0.0 else 1.0
1824
+ scale = mag / denom
1825
+ movement_local = np.array([-lx, ly]) * scale
1826
+ perp_x, perp_y = -facing[1], facing[0]
1827
+ rotation_facing = np.array([[perp_x, perp_y], [facing[0], facing[1]]])
1828
+ movement_global = rotation_facing @ movement_local
1829
+
1830
+ movement = [movement_global[0], movement_global[1], 0.0]
1831
+
1832
+ upper_body_position = None
1833
+ left_hand_position = None
1834
+ right_hand_position = None
1835
+ if stream_mode == StreamMode.PLANNER_FROZEN_UPPER_BODY:
1836
+ upper_body_position = self.feedback_reader.upper_body_position_target
1837
+ left_hand_position = self.feedback_reader.left_hand_position_target
1838
+ right_hand_position = self.feedback_reader.right_hand_position_target
1839
+
1840
+ vr_3pt_position = None
1841
+ vr_3pt_orientation = None
1842
+ vr_3pt_compliance = None
1843
+ if stream_mode == StreamMode.PLANNER_VR_3PT:
1844
+ sample = self.reader.get_latest()
1845
+ if sample is not None:
1846
+ print("[PlannerLoop] Sending VR 3-point pose as target")
1847
+ vr_3pt_pose = self.three_point.process_smpl_pose(sample["body_poses_np"])
1848
+ vr_3pt_position = (vr_3pt_pose[:, :3].flatten()).tolist()
1849
+ vr_3pt_orientation = vr_3pt_pose[:, 3:].flatten().tolist()
1850
+
1851
+ # Compute hand joints from trigger/grip inputs so operator can
1852
+ # control hand open/close while in VR 3PT mode
1853
+ (
1854
+ left_menu_button,
1855
+ left_trigger,
1856
+ right_trigger,
1857
+ left_grip,
1858
+ right_grip,
1859
+ ) = get_controller_inputs(self.reader)
1860
+ lh_joints, rh_joints = compute_hand_joints_from_inputs(
1861
+ self.left_hand_ik_solver,
1862
+ self.right_hand_ik_solver,
1863
+ left_trigger,
1864
+ left_grip,
1865
+ right_trigger,
1866
+ right_grip,
1867
+ )
1868
+ left_hand_position = lh_joints.reshape(-1).astype(np.float32).tolist()
1869
+ right_hand_position = rh_joints.reshape(-1).astype(np.float32).tolist()
1870
+
1871
+ msg = build_planner_message(
1872
+ mode_to_send.value,
1873
+ movement,
1874
+ facing,
1875
+ speed=speed,
1876
+ height=-1.0,
1877
+ upper_body_position=upper_body_position,
1878
+ left_hand_position=left_hand_position,
1879
+ right_hand_position=right_hand_position,
1880
+ vr_3pt_position=vr_3pt_position,
1881
+ vr_3pt_orientation=vr_3pt_orientation,
1882
+ vr_3pt_compliance=vr_3pt_compliance,
1883
+ )
1884
+ self.socket.send(msg)
1885
+ except Exception as e:
1886
+ import traceback
1887
+
1888
+ print(f"[PlannerLoop] error: {e}")
1889
+ traceback.print_exc()
1890
+ raise
1891
+
1892
+ # pacing
1893
+ now = time.time()
1894
+ sleep_t = self.dt - (now - self.last_send)
1895
+ if sleep_t > 0:
1896
+ time.sleep(sleep_t)
1897
+ self.last_send = time.time()
1898
+
1899
+
1900
+ def run_pico_manager(
1901
+ port: int = 5556,
1902
+ buffer_size: int = 15,
1903
+ num_frames_to_send: int = 5,
1904
+ target_fps: int = 50,
1905
+ use_cuda: bool = False,
1906
+ record_dir: str = "",
1907
+ record_format: str = "npz",
1908
+ zmq_feedback_host: str = "localhost",
1909
+ zmq_feedback_port: int = 5557,
1910
+ enable_vis_vr3pt: bool = False,
1911
+ with_g1_robot: bool = True,
1912
+ enable_waist_tracking: bool = False,
1913
+ enable_smpl_vis: bool = False,
1914
+ input_source: str = "xrt",
1915
+ ):
1916
+ """
1917
+ Manager: creates shared PUB socket and runs pose/planner streamers based on current mode.
1918
+ Controller input:
1919
+ A+X: Toggle between planner and pose mode
1920
+ A+B+X+Y: Toggle policy start/stop
1921
+ """
1922
+ reader = _init_input_source(input_source, buffer_size)
1923
+
1924
+ context = zmq.Context()
1925
+ socket = context.socket(zmq.PUB)
1926
+ socket.bind(f"tcp://*:{port}")
1927
+ time.sleep(0.1)
1928
+ print(f"[Manager] ZMQ socket bound to port {port}")
1929
+
1930
+ # Print available locomotion modes
1931
+ try:
1932
+ print("[Manager] Available modes:")
1933
+ for mode in LocomotionMode:
1934
+ print(f" {mode.value}: {mode.name}")
1935
+ except Exception:
1936
+ pass
1937
+
1938
+ three_point = ThreePointPose(
1939
+ enable_vis_vr3pt=enable_vis_vr3pt,
1940
+ with_g1_robot=with_g1_robot,
1941
+ enable_waist_tracking=enable_waist_tracking,
1942
+ enable_smpl_vis=enable_smpl_vis,
1943
+ log_prefix="PoseLoop",
1944
+ )
1945
+
1946
+ pose_streamer = PoseStreamer(
1947
+ socket=socket,
1948
+ reader=reader,
1949
+ three_point=three_point,
1950
+ num_frames_to_send=num_frames_to_send,
1951
+ target_fps=target_fps,
1952
+ use_cuda=use_cuda,
1953
+ record_dir=record_dir,
1954
+ record_format=record_format,
1955
+ log_prefix="PoseLoop",
1956
+ )
1957
+ planner_streamer = PlannerStreamer(
1958
+ socket=socket,
1959
+ reader=reader,
1960
+ three_point=three_point,
1961
+ poll_hz=20,
1962
+ zmq_feedback_host=zmq_feedback_host,
1963
+ zmq_feedback_port=zmq_feedback_port,
1964
+ )
1965
+
1966
+ # State machine diagram:
1967
+ #
1968
+ # Chain 1 (by_pressed enters/exits, left_axis_click toggles sub-mode):
1969
+ # POSE <--(by)--> PLANNER_FROZEN_UPPER_BODY <--(left_axis_click)--> PLANNER_VR_3PT
1970
+ # |
1971
+ # (by)--> POSE
1972
+ #
1973
+ # Chain 2 (ax_pressed enters/exits, left_axis_click toggles sub-mode):
1974
+ # POSE <--(ax)--> PLANNER <--(left_axis_click)--> PLANNER_VR_3PT
1975
+ # |
1976
+ # (ax)--> POSE
1977
+ #
1978
+ # Emergency stop from any mode: A+B+X+Y (start_combo) --> OFF
1979
+ # POSE_PAUSE: left_menu_button held --> POSE_PAUSE, released --> POSE
1980
+ #
1981
+ print("Manager controls: A+X=toggle mode, A+B+X+Y=start/stop policy")
1982
+ current_mode = StreamMode.OFF
1983
+ # Track which mode VR_3PT was entered from, so left_axis_click returns to it.
1984
+ # Will be either PLANNER or PLANNER_FROZEN_UPPER_BODY.
1985
+ vr3pt_parent_mode = StreamMode.PLANNER
1986
+ prev_toggle_dc = False
1987
+ prev_toggle_da = False
1988
+ try:
1989
+ prev_ax_pressed = False
1990
+ prev_by_pressed = False
1991
+ prev_start_combo = False
1992
+ prev_left_axis_click = False
1993
+ while True:
1994
+ # Poll Pico controller for buttons/axes
1995
+ a_pressed, b_pressed, x_pressed, y_pressed = get_abxy_buttons(reader)
1996
+
1997
+ left_menu_button, _, _, left_grip_mgr, _ = get_controller_inputs(reader)
1998
+
1999
+ left_axis_click, _ = get_axis_clicks(reader)
2000
+
2001
+ # Rising edge: A+X pressed together -> toggle POSE/PLANNER mode
2002
+ ax_pressed = (a_pressed) and (x_pressed)
2003
+
2004
+ # Rising edge: B+Y pressed together -> toggle POSE/PLANNER_FROZEN_UPPER_BODY mode
2005
+ by_pressed = (b_pressed) and (y_pressed)
2006
+
2007
+ # Rising edge: A+B+X+Y pressed together -> toggle policy start/stop (planner=True)
2008
+ start_combo = (a_pressed) and (b_pressed) and (x_pressed) and (y_pressed)
2009
+
2010
+ new_mode = current_mode
2011
+ if current_mode == StreamMode.OFF:
2012
+ if start_combo and not prev_start_combo:
2013
+ new_mode = StreamMode.PLANNER
2014
+ # Calibrate VR 3pt tracking NOW: operator should be in zero-ref pose.
2015
+ # Uses the current Pico SMPL frame + FK of all-zero body joints.
2016
+ sample = reader.get_latest()
2017
+ if sample is not None:
2018
+ three_point.calibrate_now(sample["body_poses_np"])
2019
+ else:
2020
+ print("[Manager] WARNING: No SMPL data available for calibration")
2021
+
2022
+ elif current_mode == StreamMode.PLANNER:
2023
+ # Chain 2: POSE <--(ax)--> PLANNER <--(left_axis_click)--> VR_3PT
2024
+ if start_combo and not prev_start_combo:
2025
+ new_mode = StreamMode.OFF
2026
+ elif ax_pressed and not prev_ax_pressed:
2027
+ new_mode = StreamMode.POSE
2028
+ elif left_axis_click and not prev_left_axis_click:
2029
+ new_mode = StreamMode.PLANNER_VR_3PT
2030
+
2031
+ elif current_mode == StreamMode.POSE:
2032
+ if start_combo and not prev_start_combo:
2033
+ new_mode = StreamMode.OFF
2034
+ elif ax_pressed and not prev_ax_pressed:
2035
+ new_mode = StreamMode.PLANNER # Enter chain 2
2036
+ elif by_pressed and not prev_by_pressed:
2037
+ new_mode = StreamMode.PLANNER_FROZEN_UPPER_BODY # Enter chain 1
2038
+ elif left_menu_button:
2039
+ new_mode = StreamMode.POSE_PAUSE
2040
+
2041
+ elif current_mode == StreamMode.PLANNER_FROZEN_UPPER_BODY:
2042
+ # Chain 1: POSE <--(by)--> FROZEN <--(left_axis_click)--> VR_3PT
2043
+ if start_combo and not prev_start_combo:
2044
+ new_mode = StreamMode.OFF
2045
+ elif by_pressed and not prev_by_pressed:
2046
+ new_mode = StreamMode.POSE
2047
+ elif left_axis_click and not prev_left_axis_click:
2048
+ new_mode = StreamMode.PLANNER_VR_3PT
2049
+
2050
+ elif current_mode == StreamMode.POSE_PAUSE:
2051
+ if start_combo and not prev_start_combo:
2052
+ new_mode = StreamMode.OFF
2053
+ elif not left_menu_button:
2054
+ new_mode = StreamMode.POSE
2055
+
2056
+ elif current_mode == StreamMode.PLANNER_VR_3PT:
2057
+ # VR_3PT is reachable from both chains:
2058
+ # left_axis_click → return to parent (PLANNER or FROZEN)
2059
+ # ax_pressed → POSE (chain 2 exit)
2060
+ # by_pressed → POSE (chain 1 exit)
2061
+ if start_combo and not prev_start_combo:
2062
+ new_mode = StreamMode.OFF
2063
+ elif left_axis_click and not prev_left_axis_click:
2064
+ new_mode = vr3pt_parent_mode # Return to parent mode
2065
+ elif ax_pressed and not prev_ax_pressed:
2066
+ new_mode = StreamMode.POSE
2067
+ elif by_pressed and not prev_by_pressed:
2068
+ new_mode = StreamMode.POSE
2069
+
2070
+ # Handle mode transitions before running loop
2071
+ if new_mode != current_mode:
2072
+ if current_mode == StreamMode.POSE:
2073
+ pose_streamer.on_mode_exit()
2074
+
2075
+ # Track parent when entering VR_3PT
2076
+ if new_mode == StreamMode.PLANNER_VR_3PT:
2077
+ vr3pt_parent_mode = current_mode
2078
+ print(f"[Manager] VR_3PT parent: {vr3pt_parent_mode.name}")
2079
+
2080
+ if new_mode == StreamMode.POSE:
2081
+ pose_streamer.reset_yaw()
2082
+ elif new_mode == StreamMode.PLANNER and current_mode != StreamMode.PLANNER_VR_3PT:
2083
+ # Only reset yaw when freshly entering PLANNER from POSE,
2084
+ # not when returning from VR_3PT sub-mode
2085
+ planner_streamer.reset_yaw()
2086
+ elif new_mode == StreamMode.PLANNER_FROZEN_UPPER_BODY:
2087
+ if current_mode != StreamMode.PLANNER_VR_3PT:
2088
+ # Freshly entering from POSE: reset yaw and grab initial targets
2089
+ planner_streamer.reset_yaw()
2090
+ # Always re-grab the latest robot state as frozen targets,
2091
+ # whether entering from POSE or returning from VR_3PT
2092
+ # (the old targets are stale after VR_3PT moved the arms)
2093
+ planner_streamer.save_upper_body_position_target()
2094
+ elif new_mode == StreamMode.PLANNER_VR_3PT:
2095
+ # Recalibrate VR tracking against the robot's actual current pose
2096
+ # (read via g1_debug feedback + FK) to prevent sudden jumps
2097
+ planner_streamer.recalibrate_for_vr3pt()
2098
+
2099
+ # Run one iteration of the new mode
2100
+ if new_mode == StreamMode.POSE:
2101
+ pose_streamer.run_once()
2102
+ elif (
2103
+ new_mode == StreamMode.PLANNER
2104
+ or new_mode == StreamMode.PLANNER_FROZEN_UPPER_BODY
2105
+ or new_mode == StreamMode.PLANNER_VR_3PT
2106
+ ):
2107
+ planner_streamer.run_once(new_mode)
2108
+
2109
+ # Make sure to send command messages after loop iteration to ensure data arrives before mode switch
2110
+ if new_mode != current_mode:
2111
+ if new_mode == StreamMode.OFF:
2112
+ socket.send(build_command_message(start=False, stop=True, planner=True))
2113
+ exit()
2114
+ elif (
2115
+ new_mode == StreamMode.PLANNER
2116
+ or new_mode == StreamMode.PLANNER_FROZEN_UPPER_BODY
2117
+ or new_mode == StreamMode.PLANNER_VR_3PT
2118
+ ):
2119
+ socket.send(build_command_message(start=True, stop=False, planner=True))
2120
+ elif new_mode == StreamMode.POSE:
2121
+ socket.send(build_command_message(start=True, stop=False, planner=False))
2122
+
2123
+ print(f"[Manager] StreamMode switch: {current_mode.name} -> {new_mode.name}")
2124
+ current_mode = new_mode
2125
+
2126
+ # Mode-independent: send manager_state for data exporter
2127
+ toggle_dc_tmp = bool(a_pressed) and left_grip_mgr > 0.5
2128
+ toggle_da_tmp = bool(b_pressed) and left_grip_mgr > 0.5
2129
+ toggle_dc = toggle_dc_tmp and not prev_toggle_dc
2130
+ toggle_da = toggle_da_tmp and not prev_toggle_da
2131
+ prev_toggle_dc = toggle_dc_tmp
2132
+ prev_toggle_da = toggle_da_tmp
2133
+ socket.send(
2134
+ pack_pose_message(
2135
+ {
2136
+ "stream_mode": np.array([current_mode.value], dtype=np.int32),
2137
+ "toggle_data_collection": np.array([toggle_dc], dtype=bool),
2138
+ "toggle_data_abort": np.array([toggle_da], dtype=bool),
2139
+ },
2140
+ topic="manager_state",
2141
+ )
2142
+ )
2143
+
2144
+ prev_ax_pressed = ax_pressed
2145
+ prev_by_pressed = by_pressed
2146
+ prev_start_combo = start_combo
2147
+ prev_left_axis_click = left_axis_click
2148
+
2149
+ except KeyboardInterrupt:
2150
+ print("\nStopping manager...")
2151
+ finally:
2152
+ # Cleanup resources
2153
+ reader.stop()
2154
+ three_point.close()
2155
+ socket.close()
2156
+ context.term()
2157
+ print("[Manager] Shutdown complete")
2158
+
2159
+
2160
+ if __name__ == "__main__":
2161
+
2162
+ import argparse
2163
+
2164
+ parser = argparse.ArgumentParser()
2165
+ parser.add_argument("--buffer_size", type=int, default=15, help="Sliding window buffer size")
2166
+ parser.add_argument("--port", type=int, default=5556, help="ZMQ server port (default: 5556)")
2167
+ parser.add_argument(
2168
+ "--num_frames_to_send", type=int, default=5, help="Number of frames to send (default: 200)"
2169
+ )
2170
+ parser.add_argument("--target_fps", type=int, default=50, help="Target loop FPS (default: 50)")
2171
+ parser.add_argument(
2172
+ "--cuda", action="store_true", help="Use CUDA for tensors and model (default: CPU)"
2173
+ )
2174
+ parser.add_argument(
2175
+ "--record_dir",
2176
+ type=str,
2177
+ default="",
2178
+ help="Directory to save sent batches (default: disabled)",
2179
+ )
2180
+ parser.add_argument(
2181
+ "--record_format",
2182
+ type=str,
2183
+ default="npz",
2184
+ help="Recording format: 'npz' or 'bin' (default: npz)",
2185
+ )
2186
+ parser.add_argument(
2187
+ "--manager",
2188
+ action="store_true",
2189
+ help="Run manager with planner and pose threads (interactive)",
2190
+ )
2191
+ parser.add_argument(
2192
+ "--zmq_feedback_host",
2193
+ type=str,
2194
+ default="localhost",
2195
+ help="ZMQ feedback host (default: localhost)",
2196
+ )
2197
+ parser.add_argument(
2198
+ "--zmq_feedback_port",
2199
+ type=int,
2200
+ default=5557,
2201
+ help="ZMQ feedback port (default: 5557)",
2202
+ )
2203
+ parser.add_argument(
2204
+ "--vr3pt_test",
2205
+ action="store_true",
2206
+ help="Run VR 3-point pose visualizer test (reference frames only)",
2207
+ )
2208
+ parser.add_argument(
2209
+ "--vr3pt_live",
2210
+ action="store_true",
2211
+ help="Capture one frame of VR 3-point pose and visualize with reference frames",
2212
+ )
2213
+ parser.add_argument(
2214
+ "--vr3pt_realtime",
2215
+ action="store_true",
2216
+ help="Run standalone real-time VR 3-point pose visualizer",
2217
+ )
2218
+ parser.add_argument(
2219
+ "--vis_vr3pt",
2220
+ action="store_true",
2221
+ help="Enable inline VR 3-point pose visualization in pose streaming mode",
2222
+ )
2223
+ parser.add_argument(
2224
+ "--vr3pt_hz",
2225
+ type=int,
2226
+ default=10,
2227
+ help="Update rate for real-time VR visualization in Hz (default: 10)",
2228
+ )
2229
+ parser.add_argument(
2230
+ "--no_g1",
2231
+ action="store_true",
2232
+ help="Disable G1 robot visualization in VR 3pt pose view (G1 is shown by default)",
2233
+ )
2234
+ parser.add_argument(
2235
+ "--waist_tracking",
2236
+ action="store_true",
2237
+ help="Enable G1 robot waist to follow VR head orientation (disabled by default for performance)",
2238
+ )
2239
+ parser.add_argument(
2240
+ "--vis_smpl",
2241
+ action="store_true",
2242
+ help="Enable SMPL body joint visualization (24 joint spheres) in the VR3pt viewer",
2243
+ )
2244
+ parser.add_argument(
2245
+ "--input-source",
2246
+ type=str,
2247
+ default="xrt",
2248
+ choices=["xrt", "isaac-teleop"],
2249
+ help=(
2250
+ "Input source: 'xrt' for XRoboToolkit SDK (default), "
2251
+ "'isaac-teleop' for in-process IsaacTeleop / CloudXR DeviceIO"
2252
+ ),
2253
+ )
2254
+ args = parser.parse_args()
2255
+
2256
+ # Standalone VR3Pt test modes (exit after finishing)
2257
+ if args.vr3pt_test:
2258
+ print("Running VR 3-point pose visualizer test...")
2259
+ run_vr3pt_visualizer_test()
2260
+ print("VR 3-point pose visualizer test completed")
2261
+ exit(0)
2262
+
2263
+ if args.vr3pt_live:
2264
+ print("Running VR 3-point pose live capture...")
2265
+ run_vr3pt_live_visualizer()
2266
+ print("VR 3-point pose live visualizer completed")
2267
+ exit(0)
2268
+
2269
+ if args.vr3pt_realtime:
2270
+ print("Running VR 3-point pose real-time visualizer...")
2271
+ run_vr3pt_realtime_visualizer(update_hz=args.vr3pt_hz)
2272
+ print("VR 3-point pose real-time visualizer completed")
2273
+ exit(0)
2274
+
2275
+ # Main execution modes
2276
+ # G1 robot visualization is enabled by default when vis_vr3pt is used
2277
+ with_g1_robot = not args.no_g1
2278
+
2279
+ if args.manager:
2280
+ run_pico_manager(
2281
+ port=args.port,
2282
+ buffer_size=args.buffer_size,
2283
+ num_frames_to_send=args.num_frames_to_send,
2284
+ target_fps=args.target_fps,
2285
+ use_cuda=args.cuda,
2286
+ record_dir=args.record_dir,
2287
+ record_format=args.record_format,
2288
+ zmq_feedback_host=args.zmq_feedback_host,
2289
+ zmq_feedback_port=args.zmq_feedback_port,
2290
+ enable_vis_vr3pt=args.vis_vr3pt,
2291
+ with_g1_robot=with_g1_robot,
2292
+ enable_waist_tracking=args.waist_tracking,
2293
+ enable_smpl_vis=args.vis_smpl,
2294
+ input_source=args.input_source,
2295
+ )
2296
+ else:
2297
+ # Run legacy single-thread pose streaming
2298
+ run_pico(
2299
+ buffer_size=args.buffer_size,
2300
+ port=args.port,
2301
+ num_frames_to_send=args.num_frames_to_send,
2302
+ target_fps=args.target_fps,
2303
+ use_cuda=args.cuda,
2304
+ record_dir=args.record_dir,
2305
+ record_format=args.record_format,
2306
+ enable_vis_vr3pt=args.vis_vr3pt,
2307
+ with_g1_robot=with_g1_robot,
2308
+ enable_waist_tracking=args.waist_tracking,
2309
+ enable_smpl_vis=args.vis_smpl,
2310
+ input_source=args.input_source,
2311
+ )
GR00T-WholeBodyControl/gear_sonic/scripts/process_dataset.py ADDED
@@ -0,0 +1,634 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Post-process a LeRobot dataset recorded by the data exporter.
3
+
4
+ Removes discarded episodes (flagged during collection) and stale SMPL frames
5
+ (all-zero teleop.smpl_pose and frozen lead-in frames that precede them) which
6
+ occur during teleop pauses or ZMQ frame drops. Can also merge multiple
7
+ recording sessions into a single dataset.
8
+
9
+ The script operates directly on the LeRobot v2.1 on-disk format
10
+ (parquet + mp4) without any external training framework dependencies.
11
+
12
+ Usage:
13
+
14
+ # Clean a single dataset in-place
15
+ python gear_sonic/scripts/process_dataset.py \\
16
+ --dataset-path outputs/my_dataset
17
+
18
+ # Clean and write to a new directory (non-destructive)
19
+ python gear_sonic/scripts/process_dataset.py \\
20
+ --dataset-path outputs/my_dataset \\
21
+ --output-path outputs/my_dataset_cleaned
22
+
23
+ # Merge multiple datasets into one (validates matching script_config)
24
+ python gear_sonic/scripts/process_dataset.py \\
25
+ --dataset-path outputs/session1 outputs/session2 outputs/session3 \\
26
+ --output-path outputs/merged_dataset
27
+
28
+ # Merge from a text file listing dataset paths (one per line)
29
+ python gear_sonic/scripts/process_dataset.py \\
30
+ --dataset-list datasets.txt \\
31
+ --output-path outputs/merged_dataset
32
+
33
+ # Skip SMPL cleaning (merge only)
34
+ python gear_sonic/scripts/process_dataset.py \\
35
+ --dataset-path outputs/session1 outputs/session2 \\
36
+ --output-path outputs/merged \\
37
+ --no-remove-stale-smpl
38
+
39
+ # Remove discarded episodes (flagged during collection via 'x' key)
40
+ python gear_sonic/scripts/process_dataset.py \\
41
+ --dataset-path outputs/my_dataset \\
42
+ --output-path outputs/my_dataset_cleaned \\
43
+ --remove-discarded
44
+ """
45
+
46
+ from dataclasses import dataclass, field
47
+ import json
48
+ from pathlib import Path
49
+ import shutil
50
+ from typing import Optional
51
+
52
+ import av
53
+ import numpy as np
54
+ import pandas as pd
55
+ import tyro
56
+
57
+
58
+ SMPL_POSE_COLUMN = "teleop.smpl_pose"
59
+
60
+
61
+ # ---------------------------------------------------------------------------
62
+ # Stale SMPL frame detection
63
+ # ---------------------------------------------------------------------------
64
+
65
+ def build_stale_mask(smpl_arr: np.ndarray) -> np.ndarray:
66
+ """Return a boolean mask where True = frame should be removed.
67
+
68
+ Marks all-zero rows AND any consecutive frozen (identical-to-next) rows
69
+ that immediately precede a zero row. Frozen runs that do NOT lead into
70
+ a zero row are left untouched — those occur naturally when the SMPL
71
+ stream publishes at a slightly lower rate than the collection loop.
72
+ """
73
+ n = len(smpl_arr)
74
+ is_zero = np.all(smpl_arr == 0, axis=1)
75
+ remove = is_zero.copy()
76
+
77
+ diffs = np.zeros(n)
78
+ diffs[1:] = np.sum(np.abs(smpl_arr[1:] - smpl_arr[:-1]), axis=1)
79
+
80
+ for i in range(n):
81
+ if is_zero[i]:
82
+ j = i - 1
83
+ while j >= 0 and diffs[j] == 0.0 and not is_zero[j]:
84
+ remove[j] = True
85
+ j -= 1
86
+
87
+ return remove
88
+
89
+
90
+ # ---------------------------------------------------------------------------
91
+ # LeRobot on-disk helpers
92
+ # ---------------------------------------------------------------------------
93
+
94
+ def load_info(dataset_path: Path) -> dict:
95
+ info_path = dataset_path / "meta" / "info.json"
96
+ with open(info_path, encoding="utf-8") as f:
97
+ return json.load(f)
98
+
99
+
100
+ def load_episodes_meta(dataset_path: Path) -> list[dict]:
101
+ episodes_path = dataset_path / "meta" / "episodes.jsonl"
102
+ episodes = []
103
+ with open(episodes_path, encoding="utf-8") as f:
104
+ for line in f:
105
+ line = line.strip()
106
+ if line:
107
+ episodes.append(json.loads(line))
108
+ return episodes
109
+
110
+
111
+ def load_tasks_meta(dataset_path: Path) -> list[dict]:
112
+ tasks_path = dataset_path / "meta" / "tasks.jsonl"
113
+ tasks = []
114
+ if tasks_path.exists():
115
+ with open(tasks_path, encoding="utf-8") as f:
116
+ for line in f:
117
+ line = line.strip()
118
+ if line:
119
+ tasks.append(json.loads(line))
120
+ return tasks
121
+
122
+
123
+ def get_parquet_path(dataset_path: Path, info: dict, episode_index: int) -> Path:
124
+ data_path_pattern = info.get("data_path", "data/chunk-{episode_chunk:03d}/episode_{episode_index:06d}.parquet")
125
+ chunks_size = info.get("chunks_size", 1000)
126
+ episode_chunk = episode_index // chunks_size
127
+ return dataset_path / data_path_pattern.format(
128
+ episode_chunk=episode_chunk, episode_index=episode_index,
129
+ )
130
+
131
+
132
+ def get_video_keys(info: dict) -> list[str]:
133
+ """Extract video keys from info.json, falling back to features if needed."""
134
+ keys = info.get("video_keys", [])
135
+ if not keys:
136
+ keys = [
137
+ k for k, v in info.get("features", {}).items()
138
+ if v.get("dtype") in ("video", "image")
139
+ ]
140
+ return keys
141
+
142
+
143
+ def get_video_paths(dataset_path: Path, info: dict, episode_index: int) -> dict[str, Path]:
144
+ video_path_pattern = info.get(
145
+ "video_path",
146
+ "videos/{video_key}/episode_{episode_index:06d}.mp4",
147
+ )
148
+ video_keys = get_video_keys(info)
149
+ chunks_size = info.get("chunks_size", 1000)
150
+ episode_chunk = episode_index // chunks_size
151
+ paths = {}
152
+ for key in video_keys:
153
+ paths[key] = dataset_path / video_path_pattern.format(
154
+ video_key=key, episode_index=episode_index,
155
+ episode_chunk=episode_chunk,
156
+ )
157
+ return paths
158
+
159
+
160
+ def filter_video_frames(video_path: Path, valid_indices: np.ndarray, fps: int):
161
+ """Re-encode a video keeping only frames at valid_indices."""
162
+ input_container = av.open(str(video_path))
163
+ input_stream = input_container.streams.video[0]
164
+
165
+ all_frames = []
166
+ for frame in input_container.decode(input_stream):
167
+ all_frames.append(frame.to_ndarray(format="rgb24"))
168
+ input_container.close()
169
+
170
+ if len(all_frames) == 0:
171
+ return
172
+
173
+ filtered = [all_frames[i] for i in valid_indices if i < len(all_frames)]
174
+ if len(filtered) == 0:
175
+ return
176
+
177
+ tmp_path = video_path.with_suffix(".tmp.mp4")
178
+ output_container = av.open(str(tmp_path), mode="w")
179
+ output_stream = output_container.add_stream("h264", rate=fps)
180
+ h, w = filtered[0].shape[:2]
181
+ output_stream.width = w
182
+ output_stream.height = h
183
+ output_stream.pix_fmt = "yuv420p"
184
+
185
+ for img in filtered:
186
+ frame = av.VideoFrame.from_ndarray(img, format="rgb24")
187
+ for packet in output_stream.encode(frame):
188
+ output_container.mux(packet)
189
+ for packet in output_stream.encode():
190
+ output_container.mux(packet)
191
+ output_container.close()
192
+
193
+ tmp_path.replace(video_path)
194
+
195
+
196
+ # ---------------------------------------------------------------------------
197
+ # Script config validation
198
+ # ---------------------------------------------------------------------------
199
+
200
+ def validate_script_configs(dataset_paths: list[Path]) -> dict | None:
201
+ """Check that all datasets share the same script_config.
202
+
203
+ Returns the common config if they match, or raises an error with
204
+ details about which datasets differ.
205
+ """
206
+ configs = {}
207
+ for ds_path in dataset_paths:
208
+ info = load_info(ds_path)
209
+ sc = info.get("script_config")
210
+ if sc is not None:
211
+ configs[ds_path.name] = sc
212
+
213
+ if not configs:
214
+ return None
215
+
216
+ canonical = json.dumps(next(iter(configs.values())), sort_keys=True)
217
+ mismatched = []
218
+ for name, cfg in configs.items():
219
+ if json.dumps(cfg, sort_keys=True) != canonical:
220
+ mismatched.append(name)
221
+
222
+ if mismatched:
223
+ print("\nERROR: script_config mismatch across datasets.")
224
+ print("The following datasets have different robot configurations:\n")
225
+ ref_name = next(iter(configs.keys()))
226
+ print(f" Reference: {ref_name}")
227
+ for name in mismatched:
228
+ print(f" Differs: {name}")
229
+ print(
230
+ "\nDatasets recorded with different robot configurations cannot be "
231
+ "merged. Verify that all sessions used the same robot setup."
232
+ )
233
+ raise SystemExit(1)
234
+
235
+ return next(iter(configs.values()))
236
+
237
+
238
+ # ---------------------------------------------------------------------------
239
+ # Core processing
240
+ # ---------------------------------------------------------------------------
241
+
242
+ def process_single_dataset(
243
+ dataset_path: Path,
244
+ remove_stale_smpl: bool,
245
+ remove_discarded: bool = False,
246
+ episode_index_offset: int = 0,
247
+ ) -> dict:
248
+ """Process one dataset: optionally clean stale SMPL frames.
249
+
250
+ Returns stats dict and the list of (parquet_df, video_paths, episode_meta)
251
+ tuples for merging.
252
+ """
253
+ info = load_info(dataset_path)
254
+ episodes_meta = load_episodes_meta(dataset_path)
255
+ fps = info.get("fps", 50)
256
+
257
+ discarded_indices = set(info.get("discarded_episode_indices", [])) if remove_discarded else set()
258
+
259
+ stats = {
260
+ "total_episodes": len(episodes_meta),
261
+ "episodes_with_stale": 0,
262
+ "total_frames": 0,
263
+ "frames_removed": 0,
264
+ "zero_frames": 0,
265
+ "frozen_leadin_frames": 0,
266
+ "episodes_dropped": 0,
267
+ "episodes_discarded": 0,
268
+ }
269
+ processed_episodes = []
270
+
271
+ for ep_meta in episodes_meta:
272
+ ep_idx = ep_meta["episode_index"]
273
+
274
+ if ep_idx in discarded_indices:
275
+ stats["episodes_discarded"] += 1
276
+ print(f" Episode {ep_idx}: discarded during collection — removing")
277
+ continue
278
+
279
+ parquet_path = get_parquet_path(dataset_path, info, ep_idx)
280
+ video_paths = get_video_paths(dataset_path, info, ep_idx)
281
+
282
+ if not parquet_path.exists():
283
+ print(f" WARNING: Missing parquet for episode {ep_idx}, skipping")
284
+ continue
285
+
286
+ df = pd.read_parquet(parquet_path)
287
+ ep_len = len(df)
288
+ stats["total_frames"] += ep_len
289
+
290
+ valid_indices = None
291
+
292
+ if remove_stale_smpl and SMPL_POSE_COLUMN in df.columns:
293
+ smpl_arr = np.vstack(
294
+ [np.asarray(x, dtype=np.float32) for x in df[SMPL_POSE_COLUMN]]
295
+ )
296
+ mask = build_stale_mask(smpl_arr)
297
+ n_remove = int(mask.sum())
298
+ n_zero = int(np.all(smpl_arr == 0, axis=1).sum())
299
+ n_frozen = n_remove - n_zero
300
+
301
+ if n_remove > 0:
302
+ stats["episodes_with_stale"] += 1
303
+ stats["frames_removed"] += n_remove
304
+ stats["zero_frames"] += n_zero
305
+ stats["frozen_leadin_frames"] += n_frozen
306
+ pct = 100.0 * n_remove / ep_len
307
+ print(
308
+ f" Episode {ep_idx}: removing {n_remove}/{ep_len} frames "
309
+ f"({pct:.1f}%) — {n_zero} zero + {n_frozen} frozen lead-in"
310
+ )
311
+
312
+ if n_remove == ep_len:
313
+ print(f" Episode {ep_idx}: ALL frames stale — dropping episode")
314
+ stats["episodes_dropped"] += 1
315
+ continue
316
+
317
+ valid_indices = np.where(~mask)[0]
318
+ df = df.iloc[valid_indices].copy().reset_index(drop=True)
319
+ if "timestamp" in df.columns:
320
+ df["timestamp"] -= df["timestamp"].iloc[0]
321
+
322
+ new_ep_idx = ep_idx + episode_index_offset
323
+ processed_episodes.append({
324
+ "df": df,
325
+ "source_video_paths": video_paths,
326
+ "valid_indices": valid_indices,
327
+ "episode_meta": ep_meta,
328
+ "new_episode_index": new_ep_idx,
329
+ "fps": fps,
330
+ })
331
+
332
+ return stats, processed_episodes, info
333
+
334
+
335
+ def write_output_dataset(
336
+ dest_path: Path,
337
+ all_episodes: list[dict],
338
+ reference_info: dict,
339
+ tasks_meta: list[dict],
340
+ script_config: dict | None,
341
+ ):
342
+ """Write processed episodes to a new LeRobot dataset directory."""
343
+ dest_path.mkdir(parents=True, exist_ok=True)
344
+ meta_dir = dest_path / "meta"
345
+ meta_dir.mkdir(exist_ok=True)
346
+
347
+ info = reference_info.copy()
348
+ fps = info.get("fps", 50)
349
+ chunks_size = info.get("chunks_size", 1000)
350
+
351
+ if script_config is not None:
352
+ info["script_config"] = script_config
353
+
354
+ total_frames = 0
355
+ episodes_jsonl = []
356
+
357
+ for i, ep in enumerate(all_episodes):
358
+ df = ep["df"]
359
+ ep_len = len(df)
360
+
361
+ df["episode_index"] = i
362
+ df["index"] = range(total_frames, total_frames + ep_len)
363
+ df["frame_index"] = range(ep_len)
364
+ if "timestamp" in df.columns:
365
+ df["timestamp"] = [j / fps for j in range(ep_len)]
366
+
367
+ episode_chunk = i // chunks_size
368
+ data_path_pattern = info.get(
369
+ "data_path",
370
+ "data/chunk-{episode_chunk:03d}/episode_{episode_index:06d}.parquet",
371
+ )
372
+ parquet_rel = data_path_pattern.format(episode_chunk=episode_chunk, episode_index=i)
373
+ parquet_path = dest_path / parquet_rel
374
+ parquet_path.parent.mkdir(parents=True, exist_ok=True)
375
+ df.to_parquet(parquet_path)
376
+
377
+ video_keys = get_video_keys(info)
378
+ video_path_pattern = info.get(
379
+ "video_path",
380
+ "videos/{video_key}/episode_{episode_index:06d}.mp4",
381
+ )
382
+ for vkey in video_keys:
383
+ src_video = ep["source_video_paths"].get(vkey)
384
+ dst_rel = video_path_pattern.format(
385
+ video_key=vkey, episode_index=i, episode_chunk=episode_chunk,
386
+ )
387
+ dst_video = dest_path / dst_rel
388
+ dst_video.parent.mkdir(parents=True, exist_ok=True)
389
+
390
+ if src_video and src_video.exists():
391
+ if ep["valid_indices"] is not None:
392
+ shutil.copy2(src_video, dst_video)
393
+ filter_video_frames(dst_video, ep["valid_indices"], fps)
394
+ else:
395
+ shutil.copy2(src_video, dst_video)
396
+
397
+ ep_meta = {
398
+ "episode_index": i,
399
+ "tasks": ep["episode_meta"].get("tasks", []),
400
+ "length": ep_len,
401
+ }
402
+ episodes_jsonl.append(ep_meta)
403
+
404
+ total_frames += ep_len
405
+
406
+ info["total_episodes"] = len(all_episodes)
407
+ info["total_frames"] = total_frames
408
+ info.pop("discarded_episode_indices", None)
409
+
410
+ with open(meta_dir / "info.json", "w", encoding="utf-8") as f:
411
+ json.dump(info, f, indent=4)
412
+
413
+ with open(meta_dir / "episodes.jsonl", "w", encoding="utf-8") as f:
414
+ for ep in episodes_jsonl:
415
+ f.write(json.dumps(ep) + "\n")
416
+
417
+ if tasks_meta:
418
+ with open(meta_dir / "tasks.jsonl", "w", encoding="utf-8") as f:
419
+ for task in tasks_meta:
420
+ f.write(json.dumps(task) + "\n")
421
+
422
+ return total_frames
423
+
424
+
425
+ def copy_modality_json(source_paths: list[Path], output_path: Path):
426
+ """Copy modality.json from the first source that has one."""
427
+ for src in source_paths:
428
+ modality_path = src / "meta" / "modality.json"
429
+ if modality_path.exists():
430
+ dst = output_path / "meta" / "modality.json"
431
+ dst.parent.mkdir(parents=True, exist_ok=True)
432
+ shutil.copy2(modality_path, dst)
433
+ return
434
+
435
+
436
+ # ---------------------------------------------------------------------------
437
+ # CLI
438
+ # ---------------------------------------------------------------------------
439
+
440
+ @dataclass
441
+ class ProcessDatasetConfig:
442
+ """Post-process LeRobot datasets: clean stale SMPL frames and/or merge."""
443
+
444
+ dataset_path: list[str] = field(default_factory=list)
445
+ """One or more dataset directories to process."""
446
+
447
+ dataset_list: Optional[str] = None
448
+ """Path to a text file listing dataset directories (one per line).
449
+ Can be used instead of or in addition to --dataset-path."""
450
+
451
+ output_path: Optional[str] = None
452
+ """Output directory for the processed dataset. If not specified and a
453
+ single dataset is given, the dataset is modified in-place."""
454
+
455
+ remove_stale_smpl: bool = True
456
+ """Remove frames where teleop.smpl_pose is all zeros (stale/dropped
457
+ SMPL data) and frozen lead-in frames that precede them."""
458
+
459
+ remove_discarded: bool = True
460
+ """Remove episodes that were flagged as discarded during data collection
461
+ (stored in meta/info.json under discarded_episode_indices)."""
462
+
463
+
464
+ def main(cfg: ProcessDatasetConfig):
465
+ dataset_paths = [Path(p) for p in cfg.dataset_path]
466
+
467
+ if cfg.dataset_list:
468
+ list_file = Path(cfg.dataset_list)
469
+ with open(list_file, encoding="utf-8") as f:
470
+ for line in f:
471
+ line = line.strip()
472
+ if line and not line.startswith("#"):
473
+ dataset_paths.append(Path(line))
474
+
475
+ if not dataset_paths:
476
+ print("ERROR: No dataset paths provided. Use --dataset-path or --dataset-list.")
477
+ raise SystemExit(1)
478
+
479
+ for ds in dataset_paths:
480
+ if not ds.exists():
481
+ print(f"ERROR: Dataset path does not exist: {ds}")
482
+ raise SystemExit(1)
483
+ if not (ds / "meta" / "info.json").exists():
484
+ print(f"ERROR: Not a valid LeRobot dataset (missing meta/info.json): {ds}")
485
+ raise SystemExit(1)
486
+
487
+ merging = len(dataset_paths) > 1
488
+ in_place = cfg.output_path is None
489
+
490
+ if merging and in_place:
491
+ print("ERROR: --output-path is required when merging multiple datasets.")
492
+ raise SystemExit(1)
493
+
494
+ output_path = Path(cfg.output_path) if cfg.output_path else dataset_paths[0]
495
+
496
+ print("=" * 70)
497
+ print(" LeRobot Dataset Processor")
498
+ print("=" * 70)
499
+ print(f" Input datasets: {len(dataset_paths)}")
500
+ for ds in dataset_paths:
501
+ print(f" - {ds}")
502
+ print(f" Output: {output_path}{' (in-place)' if in_place else ''}")
503
+ print(f" Remove stale SMPL: {cfg.remove_stale_smpl}")
504
+ print(f" Remove discarded: {cfg.remove_discarded}")
505
+ print("=" * 70)
506
+
507
+ # Validate script configs match across all datasets
508
+ if merging:
509
+ print("\nValidating script_config across datasets...")
510
+ script_config = validate_script_configs(dataset_paths)
511
+ print(" All datasets have matching robot configurations.\n")
512
+ else:
513
+ info = load_info(dataset_paths[0])
514
+ script_config = info.get("script_config")
515
+
516
+ # Collect tasks from all datasets (deduplicated)
517
+ all_tasks_meta: list[dict] = []
518
+ seen_task_ids: set = set()
519
+ for ds in dataset_paths:
520
+ for task in load_tasks_meta(ds):
521
+ tid = task.get("task_index", id(task))
522
+ if tid not in seen_task_ids:
523
+ all_tasks_meta.append(task)
524
+ seen_task_ids.add(tid)
525
+
526
+ # Process each dataset
527
+ all_episodes = []
528
+ total_stats = {
529
+ "total_episodes": 0,
530
+ "episodes_with_stale": 0,
531
+ "total_frames": 0,
532
+ "frames_removed": 0,
533
+ "zero_frames": 0,
534
+ "frozen_leadin_frames": 0,
535
+ "episodes_dropped": 0,
536
+ "episodes_discarded": 0,
537
+ }
538
+ reference_info = None
539
+
540
+ for ds_path in dataset_paths:
541
+ print(f"\nProcessing: {ds_path}")
542
+ stats, episodes, info = process_single_dataset(
543
+ ds_path,
544
+ remove_stale_smpl=cfg.remove_stale_smpl,
545
+ remove_discarded=cfg.remove_discarded,
546
+ episode_index_offset=len(all_episodes),
547
+ )
548
+
549
+ if reference_info is None:
550
+ reference_info = info
551
+
552
+ all_episodes.extend(episodes)
553
+ for key in total_stats:
554
+ total_stats[key] += stats[key]
555
+
556
+ if not all_episodes:
557
+ print("\nERROR: No valid episodes after processing.")
558
+ raise SystemExit(1)
559
+
560
+ # Write output
561
+ if in_place:
562
+ # In-place: rewrite parquet files and re-encode videos
563
+ print(f"\nRewriting dataset in-place at {output_path}...")
564
+ ds_info = load_info(output_path)
565
+ fps = ds_info.get("fps", 50)
566
+
567
+ # Delete files for discarded episodes
568
+ if cfg.remove_discarded:
569
+ discarded_indices = set(ds_info.get("discarded_episode_indices", []))
570
+ for ep_idx in discarded_indices:
571
+ parquet_path = get_parquet_path(output_path, ds_info, ep_idx)
572
+ if parquet_path.exists():
573
+ parquet_path.unlink()
574
+ video_paths = get_video_paths(output_path, ds_info, ep_idx)
575
+ for _vkey, vpath in video_paths.items():
576
+ if vpath.exists():
577
+ vpath.unlink()
578
+
579
+ for ep in all_episodes:
580
+ ep_idx = ep["episode_meta"]["episode_index"]
581
+ parquet_path = get_parquet_path(output_path, ds_info, ep_idx)
582
+ ep["df"].to_parquet(parquet_path)
583
+
584
+ if ep["valid_indices"] is not None:
585
+ video_paths = get_video_paths(output_path, ds_info, ep_idx)
586
+ for _vkey, vpath in video_paths.items():
587
+ if vpath.exists():
588
+ filter_video_frames(vpath, ep["valid_indices"], fps)
589
+
590
+ # Update episode metadata
591
+ episodes_meta = []
592
+ for ep in all_episodes:
593
+ meta = ep["episode_meta"].copy()
594
+ meta["length"] = len(ep["df"])
595
+ episodes_meta.append(meta)
596
+
597
+ with open(output_path / "meta" / "episodes.jsonl", "w", encoding="utf-8") as f:
598
+ for em in episodes_meta:
599
+ f.write(json.dumps(em) + "\n")
600
+
601
+ ds_info["total_frames"] = sum(len(ep["df"]) for ep in all_episodes)
602
+ ds_info["total_episodes"] = len(all_episodes)
603
+ if cfg.remove_discarded:
604
+ ds_info.pop("discarded_episode_indices", None)
605
+ with open(output_path / "meta" / "info.json", "w", encoding="utf-8") as f:
606
+ json.dump(ds_info, f, indent=4)
607
+ else:
608
+ print(f"\nWriting output dataset to {output_path}...")
609
+ write_output_dataset(
610
+ output_path, all_episodes, reference_info, all_tasks_meta, script_config,
611
+ )
612
+ copy_modality_json(dataset_paths, output_path)
613
+
614
+ # Print summary
615
+ kept = total_stats["total_frames"] - total_stats["frames_removed"]
616
+ kept_episodes = total_stats["total_episodes"] - total_stats["episodes_dropped"] - total_stats["episodes_discarded"]
617
+
618
+ print("\n" + "=" * 70)
619
+ print(" Processing complete!")
620
+ print("=" * 70)
621
+ print(f" Episodes: {kept_episodes} kept / {total_stats['total_episodes']} total"
622
+ f" ({total_stats['episodes_dropped']} dropped, {total_stats['episodes_discarded']} discarded)")
623
+ print(f" Frames: {kept} kept / {total_stats['total_frames']} total"
624
+ f" ({total_stats['frames_removed']} removed)")
625
+ if total_stats["frames_removed"] > 0:
626
+ print(f" Zero SMPL: {total_stats['zero_frames']}")
627
+ print(f" Frozen lead-in: {total_stats['frozen_leadin_frames']}")
628
+ print(f" Episodes affected: {total_stats['episodes_with_stale']}")
629
+ print(f" Output: {output_path}")
630
+ print("=" * 70)
631
+
632
+
633
+ if __name__ == "__main__":
634
+ main(tyro.cli(ProcessDatasetConfig))
GR00T-WholeBodyControl/gear_sonic/scripts/run_camera_viewer.py ADDED
@@ -0,0 +1,264 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ ROS-free camera viewer with optional recording.
3
+
4
+ Connects to a ZMQ camera server (MuJoCo sim SensorServer or real robot camera)
5
+ and displays live camera feeds using OpenCV. Supports recording to MP4.
6
+
7
+ Virtual environment setup (run from repo root):
8
+ bash install_scripts/install_data_collection.sh
9
+ source .venv_data_collection/bin/activate
10
+
11
+ Usage:
12
+ python gear_sonic/scripts/run_camera_viewer.py --camera-host localhost --camera-port 5555
13
+
14
+ Headless recording:
15
+ python gear_sonic/scripts/run_camera_viewer.py --no-display --record --duration 10
16
+
17
+ Controls (OpenCV window must be focused):
18
+ R - Start/stop recording
19
+ Q - Quit
20
+
21
+ Output structure:
22
+ camera_recordings/
23
+ └── rec_20260403_143052/
24
+ ├── ego_view.mp4
25
+ └── head_left_color_image.mp4
26
+ """
27
+
28
+ from dataclasses import dataclass
29
+ from pathlib import Path
30
+ import time
31
+ from typing import Optional
32
+
33
+ import cv2
34
+ import numpy as np
35
+ import tyro
36
+
37
+ from gear_sonic.camera.composed_camera import ComposedCameraClientSensor
38
+
39
+
40
+ @dataclass
41
+ class CameraViewerConfig:
42
+ """CLI config for the ROS-free camera viewer."""
43
+
44
+ camera_host: str = "localhost"
45
+ """Camera server hostname."""
46
+
47
+ camera_port: int = 5555
48
+ """Camera server port."""
49
+
50
+ fps: int = 30
51
+ """Target display refresh rate (Hz)."""
52
+
53
+ output_path: Optional[str] = None
54
+ """Output directory for recordings. Auto-creates 'camera_recordings/' if not set."""
55
+
56
+ codec: str = "mp4v"
57
+ """Video codec for recording (e.g., 'mp4v', 'XVID')."""
58
+
59
+ max_display_width: int = 640
60
+ """Max width per camera tile in the display window."""
61
+
62
+ display: bool = True
63
+ """Show the OpenCV preview window. Disable this on headless servers."""
64
+
65
+ record: bool = False
66
+ """Start recording immediately instead of waiting for the R key."""
67
+
68
+ duration: Optional[float] = None
69
+ """Stop automatically after this many seconds of recording."""
70
+
71
+
72
+ def main(config: CameraViewerConfig):
73
+ if not config.display and not config.record:
74
+ raise ValueError("--no-display requires --record")
75
+ if config.duration is not None and not config.record:
76
+ raise ValueError("--duration requires --record")
77
+ if config.duration is not None and config.duration <= 0:
78
+ raise ValueError("--duration must be greater than zero")
79
+
80
+ client = ComposedCameraClientSensor(server_ip=config.camera_host, port=config.camera_port)
81
+
82
+ print("Waiting for first camera frame...")
83
+ sample = None
84
+ for _ in range(100):
85
+ sample = client.read(blocking=False)
86
+ if sample and sample.get("images"):
87
+ break
88
+ time.sleep(0.1)
89
+
90
+ if sample is None or not sample.get("images"):
91
+ print("ERROR: No camera frames received after 10s. Check the camera server.")
92
+ return
93
+
94
+ camera_names = sorted(sample["images"].keys())
95
+ print(f"Detected {len(camera_names)} camera stream(s): {camera_names}")
96
+
97
+ output_dir = Path(config.output_path) if config.output_path else Path("camera_recordings")
98
+
99
+ is_recording = False
100
+ video_writers: dict[str, cv2.VideoWriter] = {}
101
+ frame_count = 0
102
+ recording_start_time = 0.0
103
+ recording_dir = Path(".")
104
+ loop_period = 1.0 / config.fps
105
+
106
+ window_name = "SONIC Camera Viewer"
107
+
108
+ print(f"Target FPS: {config.fps}")
109
+ print(f"Recordings will be saved to: {output_dir}")
110
+ if config.display:
111
+ print("Controls: R = start/stop recording, Q = quit")
112
+
113
+ if config.record:
114
+ recording_dir = output_dir / f"rec_{time.strftime('%Y%m%d_%H%M%S')}"
115
+ recording_dir.mkdir(parents=True, exist_ok=True)
116
+
117
+ fourcc = cv2.VideoWriter_fourcc(*config.codec)
118
+ for name in camera_names:
119
+ img = sample["images"].get(name)
120
+ if img is not None:
121
+ h, w = img.shape[:2]
122
+ path = recording_dir / f"{name}.mp4"
123
+ writer = cv2.VideoWriter(str(path), fourcc, config.fps, (w, h))
124
+ if not writer.isOpened():
125
+ raise RuntimeError(f"Failed to open video writer: {path}")
126
+ video_writers[name] = writer
127
+
128
+ is_recording = True
129
+ recording_start_time = time.time()
130
+ print(f"Recording started: {recording_dir}")
131
+
132
+ try:
133
+ while True:
134
+ t_start = time.monotonic()
135
+
136
+ image_data = client.read(blocking=False)
137
+ if image_data is None or not image_data.get("images"):
138
+ elapsed = time.monotonic() - t_start
139
+ remaining = loop_period - elapsed
140
+ if remaining > 0:
141
+ time.sleep(remaining)
142
+ continue
143
+
144
+ tiles = []
145
+ for name in camera_names:
146
+ img = image_data["images"].get(name)
147
+ if img is None:
148
+ continue
149
+
150
+ if img.shape[2] == 3:
151
+ img_bgr = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
152
+ else:
153
+ img_bgr = img
154
+
155
+ if is_recording and name in video_writers:
156
+ video_writers[name].write(img_bgr)
157
+
158
+ h, w = img_bgr.shape[:2]
159
+ if w > config.max_display_width:
160
+ scale = config.max_display_width / w
161
+ img_bgr = cv2.resize(
162
+ img_bgr, (config.max_display_width, int(h * scale))
163
+ )
164
+
165
+ label = f"{name}"
166
+ if is_recording:
167
+ label = f"[REC] {name}"
168
+ cv2.putText(
169
+ img_bgr, label, (10, 25),
170
+ cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2,
171
+ )
172
+ tiles.append(img_bgr)
173
+
174
+ if tiles:
175
+ max_h = max(t.shape[0] for t in tiles)
176
+ padded = []
177
+ for t in tiles:
178
+ if t.shape[0] < max_h:
179
+ pad = np.zeros(
180
+ (max_h - t.shape[0], t.shape[1], 3), dtype=np.uint8
181
+ )
182
+ t = np.vstack([t, pad])
183
+ padded.append(t)
184
+ canvas = np.hstack(padded)
185
+
186
+ if is_recording:
187
+ frame_count += 1
188
+ elapsed_rec = time.time() - recording_start_time
189
+ status = f"REC {frame_count}f / {elapsed_rec:.1f}s"
190
+ cv2.putText(
191
+ canvas, status, (canvas.shape[1] - 300, 25),
192
+ cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2,
193
+ )
194
+
195
+ if config.display:
196
+ cv2.imshow(window_name, canvas)
197
+
198
+ key = cv2.waitKey(1) & 0xFF if config.display else 0xFF
199
+
200
+ if key == ord("q"):
201
+ print("Quit requested.")
202
+ break
203
+ elif key == ord("r"):
204
+ if not is_recording:
205
+ recording_dir = output_dir / f"rec_{time.strftime('%Y%m%d_%H%M%S')}"
206
+ recording_dir.mkdir(parents=True, exist_ok=True)
207
+
208
+ fourcc = cv2.VideoWriter_fourcc(*config.codec)
209
+ video_writers = {}
210
+ for name in camera_names:
211
+ img = image_data["images"].get(name)
212
+ if img is not None:
213
+ h, w = img.shape[:2]
214
+ path = recording_dir / f"{name}.mp4"
215
+ video_writers[name] = cv2.VideoWriter(
216
+ str(path), fourcc, config.fps, (w, h)
217
+ )
218
+
219
+ is_recording = True
220
+ recording_start_time = time.time()
221
+ frame_count = 0
222
+ print(f"Recording started: {recording_dir}")
223
+ else:
224
+ is_recording = False
225
+ for writer in video_writers.values():
226
+ writer.release()
227
+ video_writers = {}
228
+ duration = time.time() - recording_start_time
229
+ print(
230
+ f"Recording stopped - {duration:.1f}s, {frame_count} frames "
231
+ f"-> {recording_dir}"
232
+ )
233
+
234
+ if (
235
+ is_recording
236
+ and config.duration is not None
237
+ and time.time() - recording_start_time >= config.duration
238
+ ):
239
+ print(f"Recording duration reached: {config.duration:.1f}s")
240
+ break
241
+
242
+ elapsed = time.monotonic() - t_start
243
+ remaining = loop_period - elapsed
244
+ if remaining > 0:
245
+ time.sleep(remaining)
246
+
247
+ except KeyboardInterrupt:
248
+ print("\nExiting...")
249
+ finally:
250
+ if video_writers:
251
+ for writer in video_writers.values():
252
+ writer.release()
253
+ if is_recording:
254
+ duration = time.time() - recording_start_time
255
+ print(f"Final recording: {duration:.1f}s, {frame_count} frames")
256
+
257
+ client.close()
258
+ if config.display:
259
+ cv2.destroyAllWindows()
260
+
261
+
262
+ if __name__ == "__main__":
263
+ config = tyro.cli(CameraViewerConfig)
264
+ main(config)
GR00T-WholeBodyControl/gear_sonic/scripts/run_data_exporter.py ADDED
@@ -0,0 +1,963 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Sonic VLA data exporter for G1 -- NO ROS 2 DEPENDENCY.
3
+
4
+ All data sources use ZMQ:
5
+ 1. Robot state -> ZMQ SUB on ``g1_debug`` topic (port 5557, from C++ zmq_output_handler)
6
+ 2. SMPL pose -> ZMQ SUB on ``pose`` topic (port 5556, from pico_manager_thread_server)
7
+ 3. Camera -> ZMQ/TCP via ComposedCameraClientSensor
8
+
9
+ Robot config (``script_config`` in info.json) is read from the ``robot_config``
10
+ ZMQ topic re-published every ~2 s by the C++ process. If the config is not
11
+ received within the timeout the exporter exits with an error.
12
+
13
+ Virtual environment setup (run from repo root):
14
+ bash install_scripts/install_data_collection.sh
15
+ source .venv_data_collection/bin/activate
16
+
17
+ Usage (from repo root):
18
+ python gear_sonic/scripts/run_data_exporter.py --task-prompt "pick up the cup"
19
+ python gear_sonic/scripts/run_data_exporter.py --task-prompt "walk forward" --dataset-name my_session
20
+ """
21
+
22
+ from collections import deque
23
+ from dataclasses import dataclass
24
+ from datetime import datetime
25
+ import json
26
+ import time
27
+
28
+ import numpy as np
29
+ from scipy.spatial.transform import Rotation as R
30
+ import tyro
31
+ import zmq
32
+
33
+ from gear_sonic.data.exporter import Gr00tDataExporter
34
+ from gear_sonic.data.features_sonic_vla import (
35
+ get_features_sonic_vla,
36
+ get_g1_robot_model,
37
+ get_modality_config_sonic_vla,
38
+ get_wrist_camera_features,
39
+ get_wrist_camera_modality_config,
40
+ )
41
+ from gear_sonic.camera.composed_camera import ComposedCameraClientSensor
42
+ from gear_sonic.utils.data_collection.episode_state import EpisodeState
43
+ from gear_sonic.utils.data_collection.keyboard_subscriber import ZMQKeyboardSubscriber
44
+ from gear_sonic.utils.data_collection.telemetry import Telemetry
45
+ from gear_sonic.utils.data_collection.text_to_speech import TextToSpeech
46
+ from gear_sonic.utils.data_collection.transforms import compute_projected_gravity, quat_to_rot6d
47
+ from gear_sonic.utils.data_collection.zmq_state_subscriber import (
48
+ ZMQStateSubscriber,
49
+ poll_robot_config_zmq,
50
+ )
51
+
52
+ # ---------------------------------------------------------------------------
53
+ # Config
54
+ # ---------------------------------------------------------------------------
55
+
56
+
57
+ @dataclass
58
+ class SonicDataExporterConfig:
59
+ """CLI config for the ROS-free Sonic data exporter."""
60
+
61
+ # Dataset
62
+ dataset_name: str | None = None
63
+ """Dataset name (auto-generated if creating new)."""
64
+
65
+ task_prompt: str = "demo"
66
+ """Language task prompt."""
67
+
68
+ root_output_dir: str = "outputs"
69
+ """Root output directory."""
70
+
71
+ data_collection_frequency: int = 50
72
+ """Data collection frequency (Hz)."""
73
+
74
+
75
+ # Camera
76
+ camera_host: str = "localhost"
77
+ """Camera server host."""
78
+
79
+ camera_port: int = 5555
80
+ """Camera server port."""
81
+
82
+ # ZMQ: Sonic / SMPL pose (from pico_manager_thread_server)
83
+ sonic_zmq_host: str = "localhost"
84
+ """ZMQ host for Sonic SMPL pose messages."""
85
+
86
+ sonic_zmq_port: int = 5556
87
+ """ZMQ port for Sonic SMPL pose messages."""
88
+
89
+ # ZMQ: Robot state (from C++ zmq_output_handler, g1_debug topic)
90
+ state_zmq_host: str = "localhost"
91
+ """ZMQ host for robot state (g1_debug topic from C++ deploy)."""
92
+
93
+ state_zmq_port: int = 5557
94
+ """ZMQ port for robot state (same socket as robot_config topic)."""
95
+
96
+ # Robot config
97
+ robot_config_timeout: float = 0
98
+ """Seconds to wait for the ZMQ robot_config message at startup (0 = wait forever)."""
99
+
100
+ record_wrist_cameras: bool = False
101
+ """Record wrist camera streams (left_wrist, right_wrist). Requires cameras to be available."""
102
+
103
+ text_to_speech: bool = True
104
+ """Use text-to-speech voice feedback."""
105
+
106
+
107
+ # ---------------------------------------------------------------------------
108
+ # Helpers
109
+ # ---------------------------------------------------------------------------
110
+
111
+
112
+ class TimeDeltaException(Exception):
113
+ def __init__(self, failure_count: int, reset_timeout_sec: float):
114
+ self.failure_count = failure_count
115
+ self.reset_timeout_sec = reset_timeout_sec
116
+ self.message = f"{self.failure_count} failures in {self.reset_timeout_sec} seconds"
117
+ super().__init__(self.message)
118
+
119
+
120
+ def unpack_pose_message(packed_data: bytes, topic: str = "pose") -> dict:
121
+ """Unpack a single-frame packed message from pico_manager_thread_server.
122
+
123
+ Wire format: [topic_prefix][1280-byte JSON header][concatenated binary fields]
124
+ """
125
+ HEADER_SIZE = 1280
126
+
127
+ topic_bytes = topic.encode("utf-8")
128
+ if not packed_data.startswith(topic_bytes):
129
+ raise ValueError(f"Message does not start with expected topic '{topic}'")
130
+
131
+ offset = len(topic_bytes)
132
+ if len(packed_data) < offset + HEADER_SIZE:
133
+ raise ValueError(f"Packed data too small: {len(packed_data)} < {offset + HEADER_SIZE}")
134
+
135
+ header_bytes = packed_data[offset : offset + HEADER_SIZE]
136
+ null_idx = header_bytes.find(b"\x00")
137
+ if null_idx > 0:
138
+ header_bytes = header_bytes[:null_idx]
139
+
140
+ header = json.loads(header_bytes.decode("utf-8"))
141
+ fields = header.get("fields", [])
142
+
143
+ result = {"version": header.get("v", 0), "endian": header.get("endian", "le")}
144
+ current_offset = offset + HEADER_SIZE
145
+ dtype_map = {
146
+ "f32": np.float32,
147
+ "f64": np.float64,
148
+ "i32": np.int32,
149
+ "i64": np.int64,
150
+ "bool": bool,
151
+ }
152
+
153
+ for field in fields:
154
+ dtype = dtype_map.get(field["dtype"], np.float32)
155
+ shape = tuple(field["shape"])
156
+ n_bytes = int(np.prod(shape)) * np.dtype(dtype).itemsize
157
+ result[field["name"]] = (
158
+ np.frombuffer(packed_data[current_offset : current_offset + n_bytes], dtype=dtype)
159
+ .reshape(shape)
160
+ .copy()
161
+ )
162
+ current_offset += n_bytes
163
+
164
+ return result
165
+
166
+
167
+ class TimingThresholdMonitor:
168
+ def __init__(self, max_failures=3, reset_timeout_sec=5, time_delta=0.2, raise_exception=False):
169
+ self.max_failures = max_failures
170
+ self.reset_timeout_sec = reset_timeout_sec
171
+ self.failure_count = 0
172
+ self.last_failure_time = 0
173
+ self.time_delta = time_delta
174
+ self.raise_exception = raise_exception
175
+
176
+ def reset(self):
177
+ self.failure_count = 0
178
+ self.last_failure_time = 0
179
+
180
+ def log_time_delta(self, time_delta_sec: float):
181
+ time_delta = abs(time_delta_sec)
182
+ if time_delta > self.time_delta:
183
+ self.failure_count += 1
184
+ self.last_failure_time = time.monotonic()
185
+
186
+ if self.is_threshold_exceeded():
187
+ print(
188
+ f"Time delta exception: {self.failure_count} failures in "
189
+ f"{self.reset_timeout_sec} seconds, time delta: {time_delta}"
190
+ )
191
+ if self.raise_exception:
192
+ raise TimeDeltaException(self.failure_count, self.reset_timeout_sec)
193
+
194
+ def is_threshold_exceeded(self):
195
+ if self.failure_count >= self.max_failures:
196
+ return True
197
+ if time.monotonic() - self.last_failure_time > self.reset_timeout_sec:
198
+ self.reset()
199
+ return False
200
+
201
+
202
+ # ---------------------------------------------------------------------------
203
+ # Data Collector
204
+ # ---------------------------------------------------------------------------
205
+
206
+
207
+ class GrootDataCollector:
208
+ """Collects data from G1 robot in Sonic CPP + SMPL mode -- no ROS 2.
209
+
210
+ Data sources (all ZMQ):
211
+ - ``g1_debug`` topic -> proprio (body_q, hand_q, actions, base_quat, ...)
212
+ - ``pose`` topic -> SMPL pose (smpl_joints, body_quat_w, hand_joints, ...)
213
+ - ``planner`` topic -> planner commands (vr_position, vr_orientation, ...)
214
+ - ``manager_state`` topic -> current stream mode + toggle flags
215
+ - Camera client -> ego-view images
216
+ """
217
+
218
+ def __init__(
219
+ self,
220
+ camera_host: str,
221
+ camera_port: int,
222
+ data_exporter: Gr00tDataExporter,
223
+ robot_model,
224
+ text_to_speech=None,
225
+ frequency: int = 20,
226
+ sonic_data_zmq_host: str = "localhost",
227
+ sonic_data_zmq_port: int = 5556,
228
+ state_zmq_host: str = "localhost",
229
+ state_zmq_port: int = 5557,
230
+ ):
231
+ self.text_to_speech = text_to_speech
232
+ self.frequency = frequency
233
+ self.loop_period = 1.0 / frequency
234
+ self.data_exporter = data_exporter
235
+ self.robot_model = robot_model
236
+
237
+ self._episode_state = EpisodeState()
238
+ self._keyboard_listener = ZMQKeyboardSubscriber()
239
+
240
+ self._image_subscriber = ComposedCameraClientSensor(server_ip=camera_host, port=camera_port)
241
+
242
+ self.obs_act_buffer = deque(maxlen=100)
243
+ self.latest_image_msg = None
244
+ self.latest_proprio_msg = None
245
+ self.latest_sonic_msg = None
246
+ self.latest_planner_msg = None
247
+
248
+ self.current_stream_mode = 0
249
+
250
+ self._manager_toggle_dc = False
251
+ self._manager_toggle_da = False
252
+
253
+ self._state_subscriber = ZMQStateSubscriber(
254
+ host=state_zmq_host,
255
+ port=state_zmq_port,
256
+ )
257
+
258
+ self._sonic_zmq_ctx = None
259
+ self._sonic_zmq_socket = None
260
+ try:
261
+ self._sonic_zmq_ctx = zmq.Context()
262
+ self._sonic_zmq_socket = self._sonic_zmq_ctx.socket(zmq.SUB)
263
+ self._sonic_zmq_socket.connect(f"tcp://{sonic_data_zmq_host}:{sonic_data_zmq_port}")
264
+ self._sonic_zmq_socket.setsockopt(zmq.RCVTIMEO, 100)
265
+ self._sonic_zmq_socket.setsockopt(zmq.CONFLATE, 0)
266
+ self._sonic_zmq_socket.setsockopt(zmq.RCVHWM, 20)
267
+ self._sonic_zmq_socket.setsockopt_string(zmq.SUBSCRIBE, "pose")
268
+ self._sonic_zmq_socket.setsockopt_string(zmq.SUBSCRIBE, "planner")
269
+ self._sonic_zmq_socket.setsockopt_string(zmq.SUBSCRIBE, "manager_state")
270
+ time.sleep(0.5)
271
+ print(f"[Sonic] Connected to ZMQ at {sonic_data_zmq_host}:{sonic_data_zmq_port}")
272
+ print("[Sonic] Subscribed to: pose, planner, manager_state")
273
+ except Exception as e:
274
+ print(f"[Sonic] Warning: Failed to initialize ZMQ subscriber: {e}")
275
+ self._sonic_zmq_socket = None
276
+
277
+ self.telemetry = Telemetry(window_size=100)
278
+ self.sonic_timing_monitor = TimingThresholdMonitor(
279
+ max_failures=3, reset_timeout_sec=5, time_delta=0.1
280
+ )
281
+
282
+ self._last_latency_log_time = 0.0
283
+ self._initial_yaw = None
284
+
285
+ print(f"Recording to {self.data_exporter.meta.root}")
286
+
287
+ @property
288
+ def current_episode_index(self):
289
+ return self.data_exporter.episode_buffer["episode_index"]
290
+
291
+ def _print_and_say(self, message: str, say: bool = True, blocking: bool = False):
292
+ if self.text_to_speech is not None:
293
+ self.text_to_speech.print_and_say(message, say, blocking=blocking)
294
+ else:
295
+ print(message)
296
+
297
+ def _poll_state_zmq(self):
298
+ """Poll the ``g1_debug`` ZMQ topic for robot state (non-blocking)."""
299
+ msg = self._state_subscriber.get_msg(clear=True)
300
+ if msg is None:
301
+ return
302
+
303
+ if msg.get("ros_timestamp", 0.0) == 0.0:
304
+ msg["ros_timestamp"] = time.time()
305
+
306
+ self.latest_proprio_msg = msg
307
+
308
+ def _check_recording_commands(self):
309
+ """Check keyboard + ZMQ toggle flags for recording commands."""
310
+ key = self._keyboard_listener.read_msg()
311
+
312
+ if self._manager_toggle_da:
313
+ key = "x"
314
+ self._manager_toggle_da = False
315
+ elif self._manager_toggle_dc:
316
+ key = "c"
317
+ self._manager_toggle_dc = False
318
+
319
+ if key == "c":
320
+ self._episode_state.change_state()
321
+ if self._episode_state.get_state() == self._episode_state.RECORDING:
322
+ self._initial_yaw = None
323
+ self._print_and_say(
324
+ f"Started recording {self.current_episode_index}", blocking=False
325
+ )
326
+ elif self._episode_state.get_state() == self._episode_state.NEED_TO_SAVE:
327
+ self._print_and_say("Stopping recording, preparing to save", blocking=False)
328
+ elif self._episode_state.get_state() == self._episode_state.IDLE:
329
+ self._print_and_say("Saved episode and back to idle state", blocking=False)
330
+ elif key == "x":
331
+ if self._episode_state.get_state() == self._episode_state.RECORDING:
332
+ self.data_exporter.save_episode_as_discarded()
333
+ self._episode_state.reset_state()
334
+ self._initial_yaw = None
335
+ self._print_and_say("Discarded episode", blocking=False)
336
+
337
+ def _poll_sonic_zmq_messages(self):
338
+ """Poll ZMQ for pose, planner, and manager_state messages (non-blocking)."""
339
+ if self._sonic_zmq_socket is None:
340
+ return
341
+
342
+ max_polls = 20
343
+ for _ in range(max_polls):
344
+ try:
345
+ raw = self._sonic_zmq_socket.recv(zmq.NOBLOCK)
346
+ except zmq.Again:
347
+ break
348
+
349
+ if raw.startswith(b"manager_state"):
350
+ self._handle_manager_state(raw)
351
+ elif raw.startswith(b"planner"):
352
+ self._handle_planner_message(raw)
353
+ elif raw.startswith(b"pose"):
354
+ self._handle_pose_message(raw)
355
+
356
+ def _handle_manager_state(self, raw: bytes) -> None:
357
+ try:
358
+ data = unpack_pose_message(raw, topic="manager_state")
359
+ except Exception:
360
+ return
361
+
362
+ if "stream_mode" in data:
363
+ self.current_stream_mode = int(data["stream_mode"].flat[0])
364
+
365
+ if self._extract_bool(data, "toggle_data_collection"):
366
+ self._manager_toggle_dc = True
367
+ if self._extract_bool(data, "toggle_data_abort"):
368
+ self._manager_toggle_da = True
369
+
370
+ def _handle_planner_message(self, raw: bytes) -> None:
371
+ try:
372
+ data = unpack_pose_message(raw, topic="planner")
373
+ except Exception:
374
+ return
375
+
376
+ planner_mode = int(data["mode"].flat[0]) if "mode" in data else 0
377
+ planner_movement = (
378
+ data["movement"].flatten().astype(np.float32)
379
+ if "movement" in data and data["movement"].size == 3
380
+ else np.zeros(3, dtype=np.float32)
381
+ )
382
+ planner_facing = (
383
+ data["facing"].flatten().astype(np.float32)
384
+ if "facing" in data and data["facing"].size == 3
385
+ else np.array([1.0, 0.0, 0.0], dtype=np.float32)
386
+ )
387
+ planner_speed = float(data["speed"].flat[0]) if "speed" in data else -1.0
388
+ planner_height = float(data["height"].flat[0]) if "height" in data else -1.0
389
+
390
+ vr_3pt_position = None
391
+ if "vr_position" in data and data["vr_position"].size == 9:
392
+ vr_3pt_position = data["vr_position"].flatten().astype(np.float32)
393
+ vr_3pt_orientation = None
394
+ if "vr_orientation" in data and data["vr_orientation"].size == 12:
395
+ vr_3pt_orientation = data["vr_orientation"].flatten().astype(np.float32)
396
+
397
+ self.latest_planner_msg = {
398
+ "planner_mode": planner_mode,
399
+ "planner_movement": planner_movement,
400
+ "planner_facing": planner_facing,
401
+ "planner_speed": planner_speed,
402
+ "planner_height": planner_height,
403
+ "vr_3pt_position": vr_3pt_position,
404
+ "vr_3pt_orientation": vr_3pt_orientation,
405
+ "left_hand_joints": self._extract_hand_joints(data, "left_hand_joints"),
406
+ "right_hand_joints": self._extract_hand_joints(data, "right_hand_joints"),
407
+ "receive_timestamp": time.time(),
408
+ }
409
+
410
+ def _handle_pose_message(self, raw: bytes) -> None:
411
+ G1_L_WRIST_ROLL_IDX = 23
412
+ G1_L_WRIST_PITCH_IDX = 25
413
+ G1_L_WRIST_YAW_IDX = 27
414
+ G1_R_WRIST_ROLL_IDX = 24
415
+ G1_R_WRIST_PITCH_IDX = 26
416
+ G1_R_WRIST_YAW_IDX = 28
417
+
418
+ try:
419
+ pose_data = unpack_pose_message(raw, topic="pose")
420
+ except Exception as e:
421
+ print(f"[Sonic] Error unpacking pose message: {e}")
422
+ return
423
+
424
+ try:
425
+ if "smpl_joints" not in pose_data or len(pose_data["smpl_joints"].shape) != 3:
426
+ return
427
+
428
+ left_wrist_joints = None
429
+ right_wrist_joints = None
430
+ if "joint_pos" in pose_data and len(pose_data["joint_pos"].shape) == 2:
431
+ joint_pos = pose_data["joint_pos"][0]
432
+ left_wrist_joints = np.array(
433
+ [
434
+ joint_pos[G1_L_WRIST_ROLL_IDX],
435
+ joint_pos[G1_L_WRIST_PITCH_IDX],
436
+ joint_pos[G1_L_WRIST_YAW_IDX],
437
+ ],
438
+ dtype=np.float32,
439
+ )
440
+ right_wrist_joints = np.array(
441
+ [
442
+ joint_pos[G1_R_WRIST_ROLL_IDX],
443
+ joint_pos[G1_R_WRIST_PITCH_IDX],
444
+ joint_pos[G1_R_WRIST_YAW_IDX],
445
+ ],
446
+ dtype=np.float32,
447
+ )
448
+
449
+ frame_index = None
450
+ if "frame_index" in pose_data:
451
+ frame_index = np.array([pose_data["frame_index"].flat[0]], dtype=np.int64)
452
+
453
+ smpl_pose = np.zeros(63, dtype=np.float32)
454
+ if "smpl_pose" in pose_data:
455
+ raw_pose = pose_data["smpl_pose"]
456
+ if raw_pose.ndim == 3:
457
+ smpl_pose = raw_pose[0].flatten().astype(np.float32)
458
+ elif raw_pose.ndim == 2:
459
+ smpl_pose = raw_pose.flatten().astype(np.float32)
460
+ elif raw_pose.ndim == 1 and raw_pose.size == 63:
461
+ smpl_pose = raw_pose.astype(np.float32)
462
+
463
+ left_hand_joints = self._extract_hand_joints(pose_data, "left_hand_joints")
464
+ right_hand_joints = self._extract_hand_joints(pose_data, "right_hand_joints")
465
+
466
+ vr_3pt_position = None
467
+ if "vr_position" in pose_data and pose_data["vr_position"].size == 9:
468
+ vr_3pt_position = pose_data["vr_position"].flatten().astype(np.float32)
469
+ vr_3pt_orientation = None
470
+ if "vr_orientation" in pose_data and pose_data["vr_orientation"].size == 12:
471
+ vr_3pt_orientation = pose_data["vr_orientation"].flatten().astype(np.float32)
472
+
473
+ self.latest_sonic_msg = {
474
+ "smpl_joints": pose_data["smpl_joints"][0],
475
+ "smpl_pose": smpl_pose,
476
+ "body_quat_w": (
477
+ pose_data["body_quat_w"][0] if "body_quat_w" in pose_data else None
478
+ ),
479
+ "left_hand_joints": left_hand_joints,
480
+ "right_hand_joints": right_hand_joints,
481
+ "left_wrist_joints": left_wrist_joints,
482
+ "right_wrist_joints": right_wrist_joints,
483
+ "vr_3pt_position": vr_3pt_position,
484
+ "vr_3pt_orientation": vr_3pt_orientation,
485
+ "frame_index": frame_index,
486
+ "receive_timestamp": time.time(),
487
+ }
488
+ except Exception as e:
489
+ if not hasattr(self, "_sonic_error_count"):
490
+ self._sonic_error_count = 0
491
+ self._sonic_error_count += 1
492
+ if self._sonic_error_count == 1 or self._sonic_error_count % 100 == 0:
493
+ print(f"[Sonic] Error processing pose message: {e}")
494
+
495
+ @staticmethod
496
+ def _extract_hand_joints(pose_data: dict, key: str) -> np.ndarray:
497
+ arr = pose_data.get(key)
498
+ if arr is not None:
499
+ if arr.ndim > 1:
500
+ arr = arr[0]
501
+ return arr.astype(np.float32)
502
+ return np.zeros(7, dtype=np.float32)
503
+
504
+ @staticmethod
505
+ def _extract_bool(pose_data: dict, key: str) -> bool:
506
+ val = pose_data.get(key)
507
+ if val is None:
508
+ return False
509
+ if isinstance(val, np.ndarray):
510
+ return bool(val.flat[0])
511
+ return bool(val)
512
+
513
+ def _log_latency_periodic(
514
+ self,
515
+ sonic_latency_ms: float | None = None,
516
+ ):
517
+ current_time = time.time()
518
+ if current_time - self._last_latency_log_time >= 1.0:
519
+ self._last_latency_log_time = current_time
520
+ parts = []
521
+ if sonic_latency_ms is not None:
522
+ parts.append(f"Sonic Pose: {sonic_latency_ms:.1f}ms")
523
+ if parts:
524
+ print(f"[Latency] {', '.join(parts)}")
525
+
526
+ def _add_images_to_frame_data(self, frame_data: dict) -> None:
527
+ if self.latest_image_msg is None:
528
+ return
529
+ images = self.latest_image_msg["images"]
530
+ for feature_name, feature_info in self.data_exporter.features.items():
531
+ if feature_info.get("dtype") in ["image", "video"]:
532
+ image_key = feature_name.split(".")[-1]
533
+ if image_key not in images:
534
+ raise ValueError(
535
+ f"Required image '{image_key}' for feature '{feature_name}' "
536
+ f"not found in image message. Available: {list(images.keys())}"
537
+ )
538
+ frame_data[feature_name] = images[image_key]
539
+
540
+ def _finalize_frame(self, t_start: float) -> bool:
541
+ t_end = time.monotonic()
542
+ if t_end - t_start > (1 / self.frequency):
543
+ print(f"DataExporter Missed: {t_end - t_start} sec")
544
+
545
+ if self._episode_state.get_state() == self._episode_state.NEED_TO_SAVE:
546
+ buffer_size = self.data_exporter.episode_buffer.get("size", 0)
547
+ if buffer_size > 0:
548
+ self.data_exporter.save_episode()
549
+ self.sonic_timing_monitor.reset()
550
+ self._initial_yaw = None
551
+ self._print_and_say("Finished saving episode")
552
+ else:
553
+ self._print_and_say("Skipping save: no frames collected", say=False)
554
+ self._episode_state.change_state()
555
+ return True
556
+
557
+ def _add_data_frame(self):
558
+ t_start = time.monotonic()
559
+
560
+ if self.latest_proprio_msg is None or self.latest_image_msg is None:
561
+ self._print_and_say(
562
+ f"Waiting for message. "
563
+ f"Avail msg: proprio {self.latest_proprio_msg is not None} | "
564
+ f"image {self.latest_image_msg is not None}",
565
+ say=False,
566
+ )
567
+ return False
568
+
569
+ if self._episode_state.get_state() != self._episode_state.RECORDING:
570
+ return self._finalize_frame(t_start)
571
+
572
+ return self._add_data_frame_sonic(t_start)
573
+
574
+ def _add_data_frame_sonic(self, t_start: float) -> bool:
575
+ """Build one data frame in Sonic CPP + SMPL mode."""
576
+ assert self.latest_proprio_msg is not None
577
+ proprio = self.latest_proprio_msg
578
+
579
+ whole_q = self.robot_model.get_configuration_from_actuated_joints(
580
+ body_actuated_joint_values=proprio["body_q"],
581
+ left_hand_actuated_joint_values=proprio["left_hand_q"],
582
+ right_hand_actuated_joint_values=proprio["right_hand_q"],
583
+ )
584
+ whole_action_wbc = self.robot_model.get_configuration_from_actuated_joints(
585
+ body_actuated_joint_values=proprio["last_action"],
586
+ left_hand_actuated_joint_values=proprio["last_left_hand_action"],
587
+ right_hand_actuated_joint_values=proprio["last_right_hand_action"],
588
+ )
589
+
590
+ self.robot_model.cache_forward_kinematics(whole_q)
591
+ eef_parts = []
592
+ for side in ["left", "right"]:
593
+ placement = self.robot_model.frame_placement(
594
+ self.robot_model.supplemental_info.hand_frame_names[side]
595
+ )
596
+ pos = placement.translation[:3]
597
+ quat = R.from_matrix(placement.rotation).as_quat(scalar_first=True)
598
+ eef_parts.append(np.concatenate([pos, quat]))
599
+ observation_eef_state = np.concatenate(eef_parts)
600
+
601
+ frame_data: dict = {
602
+ "observation.state": whole_q,
603
+ "observation.eef_state": observation_eef_state,
604
+ "action.wbc": whole_action_wbc,
605
+ }
606
+
607
+ self._add_cpp_state_features(frame_data, proprio)
608
+
609
+ sonic_latency_ms = self._add_sonic_pose_features(frame_data)
610
+
611
+ self._add_images_to_frame_data(frame_data)
612
+
613
+ self._log_latency_periodic(sonic_latency_ms)
614
+
615
+ self.data_exporter.add_frame(frame_data)
616
+ return self._finalize_frame(t_start)
617
+
618
+ def _add_cpp_state_features(self, frame_data: dict, proprio: dict) -> None:
619
+ if "base_quat" in proprio:
620
+ base_quat = np.asarray(proprio["base_quat"], dtype=np.float64)
621
+ frame_data["observation.root_orientation"] = base_quat
622
+ frame_data["observation.projected_gravity"] = compute_projected_gravity(
623
+ base_quat
624
+ ).astype(np.float64)
625
+
626
+ if "init_ref_data_root_rot_array" in proprio:
627
+ frame_data["observation.cpp_rotation_offset"] = np.asarray(
628
+ proprio["init_ref_data_root_rot_array"], dtype=np.float64
629
+ )
630
+ else:
631
+ frame_data["observation.cpp_rotation_offset"] = np.array(
632
+ [1.0, 0.0, 0.0, 0.0], dtype=np.float64
633
+ )
634
+ else:
635
+ frame_data["observation.root_orientation"] = np.array(
636
+ [1.0, 0.0, 0.0, 0.0], dtype=np.float64
637
+ )
638
+ frame_data["observation.projected_gravity"] = np.array(
639
+ [0.0, 0.0, -1.0], dtype=np.float64
640
+ )
641
+ frame_data["observation.cpp_rotation_offset"] = np.array(
642
+ [1.0, 0.0, 0.0, 0.0], dtype=np.float64
643
+ )
644
+
645
+ if "init_base_quat" in proprio:
646
+ frame_data["observation.init_base_quat"] = np.asarray(
647
+ proprio["init_base_quat"], dtype=np.float64
648
+ )
649
+ else:
650
+ frame_data["observation.init_base_quat"] = np.array(
651
+ [1.0, 0.0, 0.0, 0.0], dtype=np.float64
652
+ )
653
+
654
+ if "delta_heading" in proprio:
655
+ dh = proprio["delta_heading"]
656
+ if isinstance(dh, np.ndarray):
657
+ dh = dh.item() if dh.size == 1 else dh[0]
658
+ frame_data["teleop.delta_heading"] = np.array([float(dh)], dtype=np.float64)
659
+ else:
660
+ frame_data["teleop.delta_heading"] = np.zeros(1, dtype=np.float64)
661
+
662
+ if "token_state" in proprio:
663
+ frame_data["action.motion_token"] = np.asarray(proprio["token_state"], dtype=np.float64)
664
+ else:
665
+ frame_data["action.motion_token"] = np.zeros(64, dtype=np.float64)
666
+
667
+ def _add_sonic_pose_features(self, frame_data: dict) -> float | None:
668
+ """Add teleop features based on current stream mode."""
669
+ sonic_latency_ms = None
670
+
671
+ frame_data["teleop.stream_mode"] = np.array([self.current_stream_mode], dtype=np.int32)
672
+
673
+ smpl_msg = self.latest_sonic_msg
674
+ use_smpl = False
675
+ if self.current_stream_mode in (1, 4) and smpl_msg is not None:
676
+ receive_ts = smpl_msg.get("receive_timestamp")
677
+ if receive_ts is not None:
678
+ age_sec = time.time() - receive_ts
679
+ sonic_latency_ms = age_sec * 1000
680
+ self.sonic_timing_monitor.log_time_delta(age_sec)
681
+ if sonic_latency_ms <= 100.0:
682
+ use_smpl = True
683
+ elif (self.sonic_timing_monitor.failure_count + 1) % 10 == 0:
684
+ self._print_and_say(
685
+ f"Sonic pose stale ({sonic_latency_ms:.1f}ms old), using zeros",
686
+ say=False,
687
+ )
688
+ else:
689
+ use_smpl = True
690
+
691
+ planner_msg = self.latest_planner_msg
692
+ use_planner = False
693
+ if self.current_stream_mode == 5 and planner_msg is not None:
694
+ receive_ts = planner_msg.get("receive_timestamp")
695
+ if receive_ts is not None:
696
+ age_sec = time.time() - receive_ts
697
+ planner_latency_ms = age_sec * 1000
698
+ if sonic_latency_ms is None:
699
+ sonic_latency_ms = planner_latency_ms
700
+ if planner_latency_ms <= 200.0:
701
+ use_planner = True
702
+ else:
703
+ use_planner = True
704
+
705
+ # SMPL features
706
+ if use_smpl and smpl_msg.get("smpl_joints") is not None:
707
+ joints = np.asarray(smpl_msg["smpl_joints"], dtype=np.float32)
708
+ if joints.ndim == 2:
709
+ joints = joints.flatten()
710
+ frame_data["teleop.smpl_joints"] = np.ascontiguousarray(joints, dtype=np.float32)
711
+ else:
712
+ frame_data["teleop.smpl_joints"] = np.zeros(72, dtype=np.float32)
713
+
714
+ if use_smpl and smpl_msg.get("smpl_pose") is not None:
715
+ pose = np.asarray(smpl_msg["smpl_pose"], dtype=np.float32)
716
+ if pose.ndim > 1:
717
+ pose = pose.flatten()
718
+ frame_data["teleop.smpl_pose"] = np.ascontiguousarray(pose, dtype=np.float32)
719
+ else:
720
+ frame_data["teleop.smpl_pose"] = np.zeros(63, dtype=np.float32)
721
+
722
+ if use_smpl and smpl_msg.get("body_quat_w") is not None:
723
+ body_quat_w = smpl_msg["body_quat_w"].astype(np.float32)
724
+ frame_data["teleop.body_quat_w"] = body_quat_w
725
+ frame_data["teleop.target_body_orientation"] = self._compute_target_body_orientation(
726
+ body_quat_w, frame_data
727
+ )
728
+ else:
729
+ frame_data["teleop.body_quat_w"] = np.array([1.0, 0.0, 0.0, 0.0], dtype=np.float32)
730
+ frame_data["teleop.target_body_orientation"] = quat_to_rot6d(
731
+ np.array([1.0, 0.0, 0.0, 0.0], dtype=np.float32)
732
+ )
733
+
734
+ frame_data["teleop.left_wrist_joints"] = (
735
+ smpl_msg["left_wrist_joints"].astype(np.float32)
736
+ if use_smpl and smpl_msg.get("left_wrist_joints") is not None
737
+ else np.zeros(3, dtype=np.float32)
738
+ )
739
+ frame_data["teleop.right_wrist_joints"] = (
740
+ smpl_msg["right_wrist_joints"].astype(np.float32)
741
+ if use_smpl and smpl_msg.get("right_wrist_joints") is not None
742
+ else np.zeros(3, dtype=np.float32)
743
+ )
744
+
745
+ frame_data["teleop.smpl_frame_index"] = (
746
+ smpl_msg["frame_index"].astype(np.int64)
747
+ if use_smpl and smpl_msg is not None and smpl_msg.get("frame_index") is not None
748
+ else np.array([0], dtype=np.int64)
749
+ )
750
+
751
+ hand_msg = (
752
+ smpl_msg if self.current_stream_mode in (1, 4) and smpl_msg is not None
753
+ else planner_msg if planner_msg is not None
754
+ else smpl_msg
755
+ )
756
+ frame_data["teleop.left_hand_joints"] = (
757
+ hand_msg["left_hand_joints"].astype(np.float32)
758
+ if hand_msg is not None
759
+ and hand_msg.get("left_hand_joints") is not None
760
+ else np.zeros(7, dtype=np.float32)
761
+ )
762
+ frame_data["teleop.right_hand_joints"] = (
763
+ hand_msg["right_hand_joints"].astype(np.float32)
764
+ if hand_msg is not None
765
+ and hand_msg.get("right_hand_joints") is not None
766
+ else np.zeros(7, dtype=np.float32)
767
+ )
768
+
769
+ # Planner command fields
770
+ frame_data["teleop.planner_mode"] = np.array(
771
+ [planner_msg["planner_mode"]] if use_planner else [0],
772
+ dtype=np.int32,
773
+ )
774
+ frame_data["teleop.planner_movement"] = (
775
+ planner_msg["planner_movement"].copy()
776
+ if use_planner and planner_msg.get("planner_movement") is not None
777
+ else np.zeros(3, dtype=np.float32)
778
+ )
779
+ frame_data["teleop.planner_facing"] = (
780
+ planner_msg["planner_facing"].copy()
781
+ if use_planner and planner_msg.get("planner_facing") is not None
782
+ else np.array([1.0, 0.0, 0.0], dtype=np.float32)
783
+ )
784
+ frame_data["teleop.planner_speed"] = np.array(
785
+ [planner_msg["planner_speed"]] if use_planner else [-1.0],
786
+ dtype=np.float32,
787
+ )
788
+ frame_data["teleop.planner_height"] = np.array(
789
+ [planner_msg["planner_height"]] if use_planner else [-1.0],
790
+ dtype=np.float32,
791
+ )
792
+
793
+ # VR 3-point pose
794
+ frame_data["teleop.vr_3pt_position"] = (
795
+ planner_msg["vr_3pt_position"].astype(np.float32)
796
+ if use_planner and planner_msg.get("vr_3pt_position") is not None
797
+ else np.zeros(9, dtype=np.float32)
798
+ )
799
+ if use_planner and planner_msg.get("vr_3pt_orientation") is not None:
800
+ frame_data["teleop.vr_3pt_orientation"] = quat_to_rot6d(
801
+ planner_msg["vr_3pt_orientation"].astype(np.float32)
802
+ )
803
+ else:
804
+ frame_data["teleop.vr_3pt_orientation"] = np.zeros(18, dtype=np.float32)
805
+
806
+ return sonic_latency_ms
807
+
808
+ def _compute_target_body_orientation(
809
+ self, body_quat_w: np.ndarray, frame_data: dict
810
+ ) -> np.ndarray:
811
+ """Compute yaw-normalised target body orientation as rot6d (6-dim)."""
812
+ delta_heading = float(frame_data.get("teleop.delta_heading", [0.0])[0])
813
+
814
+ body_rot = R.from_quat(body_quat_w, scalar_first=True)
815
+ target_rot = R.from_euler("z", delta_heading, degrees=False) * body_rot
816
+
817
+ euler = target_rot.as_euler("ZYX", degrees=False)
818
+ current_yaw = euler[0]
819
+
820
+ if self._initial_yaw is None:
821
+ self._initial_yaw = current_yaw
822
+
823
+ normalised_euler = np.array([current_yaw - self._initial_yaw, euler[1], euler[2]])
824
+ target_quat = (
825
+ R.from_euler("ZYX", normalised_euler, degrees=False)
826
+ .as_quat(scalar_first=True)
827
+ .astype(np.float32)
828
+ )
829
+ return quat_to_rot6d(target_quat)
830
+
831
+ def save_and_cleanup(self):
832
+ try:
833
+ self._print_and_say("saving episode done", blocking=False)
834
+ buffer_size = self.data_exporter.episode_buffer.get("size", 0)
835
+ if buffer_size > 0:
836
+ self.data_exporter.save_episode()
837
+ self._print_and_say(
838
+ f"Recording complete: {self.data_exporter.meta.root}", say=False, blocking=True
839
+ )
840
+ except Exception as e:
841
+ self._print_and_say(f"Error saving episode: {e}", blocking=True)
842
+
843
+ try:
844
+ self._state_subscriber.close()
845
+ except Exception:
846
+ pass
847
+ for sock in [self._sonic_zmq_socket]:
848
+ if sock is not None:
849
+ try:
850
+ sock.close()
851
+ except Exception:
852
+ pass
853
+ for ctx in [self._sonic_zmq_ctx]:
854
+ if ctx is not None:
855
+ try:
856
+ ctx.term()
857
+ except Exception:
858
+ pass
859
+
860
+ self._print_and_say("Shutting down data exporter...", say=False)
861
+
862
+ def run(self):
863
+ try:
864
+ while True:
865
+ t_start = time.monotonic()
866
+ with self.telemetry.timer("total_loop"):
867
+ with self.telemetry.timer("poll_state"):
868
+ self._poll_state_zmq()
869
+
870
+ with self.telemetry.timer("poll_sonic"):
871
+ self._poll_sonic_zmq_messages()
872
+
873
+ with self.telemetry.timer("poll_image"):
874
+ img_msg = self._image_subscriber.read()
875
+ if img_msg is not None:
876
+ self.latest_image_msg = img_msg
877
+
878
+ with self.telemetry.timer("add_frame"):
879
+ self._add_data_frame()
880
+
881
+ with self.telemetry.timer("check_recording_commands"):
882
+ self._check_recording_commands()
883
+
884
+ end_time = time.monotonic()
885
+
886
+ elapsed = time.monotonic() - t_start
887
+ sleep_time = self.loop_period - elapsed
888
+ if sleep_time > 0:
889
+ time.sleep(sleep_time)
890
+
891
+ if (end_time - t_start) > self.loop_period:
892
+ self.telemetry.log_timing_info(
893
+ context="Data Exporter Loop Missed", threshold=0.001
894
+ )
895
+
896
+ except KeyboardInterrupt:
897
+ print("Data exporter terminated by user")
898
+ buffer_size = self.data_exporter.episode_buffer.get("size", 0)
899
+ if buffer_size > 0:
900
+ self.data_exporter.save_episode_as_discarded()
901
+
902
+ finally:
903
+ self.save_and_cleanup()
904
+
905
+
906
+ # ---------------------------------------------------------------------------
907
+ # Entry point
908
+ # ---------------------------------------------------------------------------
909
+
910
+
911
+ def main(config: SonicDataExporterConfig):
912
+ g1_rm = get_g1_robot_model()
913
+
914
+ dataset_features = get_features_sonic_vla(g1_rm)
915
+ modality_config = get_modality_config_sonic_vla(g1_rm)
916
+
917
+ if config.record_wrist_cameras:
918
+ print("[Camera] Wrist cameras enabled — adding to dataset schema")
919
+ dataset_features.update(get_wrist_camera_features())
920
+ wrist_modality = get_wrist_camera_modality_config()
921
+ for key, value in wrist_modality.items():
922
+ if key in modality_config:
923
+ modality_config[key].update(value)
924
+ else:
925
+ modality_config[key] = value
926
+
927
+ text_to_speech = TextToSpeech() if config.text_to_speech else None
928
+
929
+ robot_config = poll_robot_config_zmq(
930
+ config.state_zmq_host, config.state_zmq_port, config.robot_config_timeout
931
+ )
932
+
933
+ data_exporter = Gr00tDataExporter.create(
934
+ save_root=f"{config.root_output_dir}/{config.dataset_name}",
935
+ fps=config.data_collection_frequency,
936
+ features=dataset_features,
937
+ modality_config=modality_config,
938
+ task=config.task_prompt,
939
+ script_config={**robot_config, "record_wrist_cameras": config.record_wrist_cameras},
940
+ )
941
+
942
+ data_collector = GrootDataCollector(
943
+ frequency=config.data_collection_frequency,
944
+ data_exporter=data_exporter,
945
+ robot_model=g1_rm,
946
+ camera_host=config.camera_host,
947
+ camera_port=config.camera_port,
948
+ text_to_speech=text_to_speech,
949
+ sonic_data_zmq_host=config.sonic_zmq_host,
950
+ sonic_data_zmq_port=config.sonic_zmq_port,
951
+ state_zmq_host=config.state_zmq_host,
952
+ state_zmq_port=config.state_zmq_port,
953
+ )
954
+ data_collector.run()
955
+
956
+
957
+ if __name__ == "__main__":
958
+ config = tyro.cli(SonicDataExporterConfig)
959
+
960
+ if config.dataset_name is None:
961
+ config.dataset_name = datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
962
+
963
+ main(config)
GR00T-WholeBodyControl/gear_sonic/scripts/run_sim_loop.py ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Entry point for running a MuJoCo simulation loop with the G1 robot model.
2
+
3
+ Parses a YAML-based WBC config via tyro CLI, instantiates the G1 robot model,
4
+ and launches the simulator (optionally with offscreen image publishing).
5
+ """
6
+
7
+ from typing import Dict
8
+
9
+ import tyro
10
+
11
+ from gear_sonic.utils.mujoco_sim.simulator_factory import SimulatorFactory, init_channel
12
+ from gear_sonic.utils.mujoco_sim.configs import SimLoopConfig
13
+ from gear_sonic.data.robot_model.instantiation.g1 import (
14
+ instantiate_g1_robot_model,
15
+ )
16
+ from gear_sonic.data.robot_model.robot_model import RobotModel
17
+
18
+ ArgsConfig = SimLoopConfig
19
+
20
+
21
+ class SimWrapper:
22
+ def __init__(self, robot_model: RobotModel, env_name: str, config: Dict[str, any], **kwargs):
23
+ self.robot_model = robot_model
24
+ self.config = config
25
+
26
+ init_channel(config=self.config)
27
+
28
+ # Create simulator using factory
29
+ self.sim = SimulatorFactory.create_simulator(
30
+ config=self.config,
31
+ env_name=env_name,
32
+ **kwargs,
33
+ )
34
+
35
+
36
+ def main(config: ArgsConfig):
37
+ wbc_config = config.load_wbc_yaml()
38
+ # NOTE: we will override the interface to local if it is not specified
39
+ wbc_config["ENV_NAME"] = config.env_name
40
+
41
+ if config.enable_image_publish:
42
+ assert (
43
+ config.enable_offscreen
44
+ ), "enable_offscreen must be True when enable_image_publish is True"
45
+
46
+ robot_model = instantiate_g1_robot_model()
47
+
48
+ sim_wrapper = SimWrapper(
49
+ robot_model=robot_model,
50
+ env_name=config.env_name,
51
+ config=wbc_config,
52
+ onscreen=wbc_config.get("ENABLE_ONSCREEN", True),
53
+ offscreen=wbc_config.get("ENABLE_OFFSCREEN", False),
54
+ enable_image_publish=config.enable_image_publish,
55
+ )
56
+ # Start simulator as independent process
57
+ SimulatorFactory.start_simulator(
58
+ sim_wrapper.sim,
59
+ as_thread=False,
60
+ enable_image_publish=config.enable_image_publish,
61
+ mp_start_method=config.mp_start_method,
62
+ camera_port=config.camera_port,
63
+ )
64
+
65
+
66
+ if __name__ == "__main__":
67
+ config = tyro.cli(ArgsConfig)
68
+ main(config)
GR00T-WholeBodyControl/gear_sonic/scripts/run_vla_inference.py ADDED
@@ -0,0 +1,777 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ VLA inference runner — NO ROS 2 DEPENDENCY.
3
+
4
+ Runs an Isaac-GR00T VLA policy against the Sonic whole-body control stack.
5
+ All communication uses ZMQ:
6
+ 1. Robot state -> ZMQ SUB on ``g1_debug`` topic (from C++ zmq_output_handler)
7
+ 2. Actions out -> ZMQ PUB (latent protocol v4: motion token + hand joints)
8
+ 3. Camera -> ZMQ/TCP via ComposedCameraClientSensor
9
+ 4. Keyboard -> ZMQ SUB via ZMQKeyboardSubscriber
10
+
11
+ Uses the Isaac-GR00T PolicyClient (ZMQ REQ/REP) to communicate with a
12
+ running PolicyServer.
13
+
14
+ Keyboard commands (received via ZMQ from the standalone keyboard publisher):
15
+ p -> pause / resume the policy loop
16
+ k -> start / stop the C++ control loop
17
+ i -> blend smoothly to initial pose (or snap if no prior token) and switch to POSE mode
18
+ t -> change prompt at runtime (publisher sends ``prompt:<text>``)
19
+ [ -> toggle left hand open/closed for initial pose
20
+ ] -> toggle right hand open/closed for initial pose
21
+ c -> start recording (handled by data exporter if running)
22
+ s -> stop recording success (handled by data exporter)
23
+ f -> stop recording failure (handled by data exporter)
24
+ """
25
+
26
+ from dataclasses import dataclass
27
+ import queue
28
+ import threading
29
+ import time
30
+
31
+ import numpy as np
32
+ import tyro
33
+ import zmq
34
+
35
+ from gear_sonic.camera.composed_camera import ComposedCameraClientSensor
36
+ from gear_sonic.data.robot_model.instantiation.g1 import instantiate_g1_robot_model
37
+ from gear_sonic.utils.data_collection.keyboard_subscriber import (
38
+ DEFAULT_ZMQ_KEYBOARD_PORT,
39
+ ZMQKeyboardSubscriber,
40
+ )
41
+ from gear_sonic.utils.data_collection.telemetry import Telemetry
42
+ from gear_sonic.utils.data_collection.transforms import compute_projected_gravity
43
+ from gear_sonic.utils.data_collection.zmq_state_subscriber import ZMQStateSubscriber
44
+ from gear_sonic.utils.inference.initial_poses import LATENT_INITIAL_MOTION_TOKEN
45
+ from gear_sonic.utils.inference.vla_utils import (
46
+ calculate_latency_compensated_index,
47
+ concat_action,
48
+ prepare_observation_for_eval,
49
+ should_trigger_new_inference,
50
+ )
51
+ from gear_sonic.utils.teleop.solver.hand.g1_gripper_ik_solver import (
52
+ G1GripperInverseKinematicsSolver,
53
+ )
54
+ from gear_sonic.utils.teleop.zmq.zmq_planner_sender import (
55
+ build_command_message,
56
+ pack_pose_message,
57
+ )
58
+
59
+
60
+ @dataclass
61
+ class InferenceConfig:
62
+ """CLI config for the VLA inference runner."""
63
+
64
+ # Policy server (Isaac-GR00T PolicyServer)
65
+ host: str = "localhost"
66
+ """The host address of the Isaac-GR00T PolicyServer."""
67
+
68
+ port: int = 5550
69
+ """The port of the Isaac-GR00T PolicyServer."""
70
+
71
+ # Control
72
+ action_publish_rate: int = 50
73
+ """Rate at which individual actions are published to the C++ control loop (Hz)."""
74
+
75
+ action_horizon: int = 40
76
+ """Action horizon of the VLA policy (number of future actions per inference)."""
77
+
78
+ rate: float = 1 / 0.4
79
+ """Rate at which we run the forward pass of the VLA policy (Hz)."""
80
+
81
+ # Camera
82
+ camera_host: str = "localhost"
83
+ """Camera server host."""
84
+
85
+ camera_port: int = 5555
86
+ """Camera server port."""
87
+
88
+ # ZMQ: Robot state (from C++ zmq_output_handler, g1_debug topic)
89
+ state_zmq_host: str = "localhost"
90
+ """ZMQ host for robot state (g1_debug topic from C++ deploy)."""
91
+
92
+ state_zmq_port: int = 5557
93
+ """ZMQ port for robot state (same socket as robot_config topic)."""
94
+
95
+ # ZMQ: Action output (latent actions to C++ control loop)
96
+ action_zmq_host: str = "localhost"
97
+ """ZMQ host for action output (PUB socket)."""
98
+
99
+ action_zmq_port: int = 5556
100
+ """ZMQ port for action output."""
101
+
102
+ # ZMQ: Keyboard input
103
+ keyboard_zmq_host: str = "localhost"
104
+ """ZMQ host for keyboard input."""
105
+
106
+ keyboard_zmq_port: int = DEFAULT_ZMQ_KEYBOARD_PORT
107
+ """ZMQ port for keyboard input."""
108
+
109
+ # Embodiment
110
+ embodiment_tag: str = "unitree_g1_sonic"
111
+ """Embodiment tag for policy inference."""
112
+
113
+ # Prompt / eval
114
+ prompt: str = "demo"
115
+ """The language prompt for the VLA policy."""
116
+
117
+ # Initial pose
118
+ initial_pose_blend_duration: float = 1.0
119
+ """Duration (seconds) for smooth interpolation to initial pose. The robot
120
+ blends from its current motion token to the initial pose token over this
121
+ period. Set to 0 to snap instantly (no blend)."""
122
+
123
+ # Debug
124
+ verbose_timing: bool = False
125
+ """Whether to always print timing info (not just when loop is slow)."""
126
+
127
+
128
+ def print_green(x):
129
+ print(f"\033[92m{x}\033[0m")
130
+
131
+
132
+ # ---------------------------------------------------------------------------
133
+ # Action packing (latent protocol v4)
134
+ # ---------------------------------------------------------------------------
135
+
136
+
137
+ def pack_latent_action_message(
138
+ motion_token: np.ndarray,
139
+ frame_index: np.ndarray,
140
+ left_hand_joints: np.ndarray = None,
141
+ right_hand_joints: np.ndarray = None,
142
+ ) -> bytes:
143
+ """Pack a single motion-token action into a ZMQ message (Protocol v4).
144
+
145
+ Args:
146
+ motion_token: Shape ``[64]`` (flat) or ``[1, 64]``.
147
+ frame_index: Shape ``[1]``.
148
+ left_hand_joints: Shape ``[7]`` or ``[1, 7]``, optional.
149
+ right_hand_joints: Shape ``[7]`` or ``[1, 7]``, optional.
150
+
151
+ Returns:
152
+ Packed ZMQ message bytes.
153
+ """
154
+ motion_token = np.asarray(motion_token, dtype=np.float32)
155
+ frame_index = np.asarray(frame_index, dtype=np.int64)
156
+
157
+ if frame_index.ndim == 0:
158
+ frame_index = np.array([frame_index], dtype=np.int64)
159
+ elif frame_index.shape[0] != 1:
160
+ frame_index = frame_index[:1]
161
+
162
+ if motion_token.ndim == 1:
163
+ motion_token = motion_token.reshape(1, -1)
164
+
165
+ pose_data = {
166
+ "token_state": motion_token,
167
+ "frame_index": frame_index,
168
+ }
169
+
170
+ if left_hand_joints is not None:
171
+ left_hand_joints = np.asarray(left_hand_joints, dtype=np.float32)
172
+ if left_hand_joints.ndim == 1:
173
+ if left_hand_joints.shape[0] != 7:
174
+ raise ValueError(
175
+ f"left_hand_joints must have shape [7], got {left_hand_joints.shape}"
176
+ )
177
+ left_hand_joints = left_hand_joints.reshape(1, 7)
178
+ pose_data["left_hand_joints"] = left_hand_joints
179
+
180
+ if right_hand_joints is not None:
181
+ right_hand_joints = np.asarray(right_hand_joints, dtype=np.float32)
182
+ if right_hand_joints.ndim == 1:
183
+ if right_hand_joints.shape[0] != 7:
184
+ raise ValueError(
185
+ f"right_hand_joints must have shape [7], got {right_hand_joints.shape}"
186
+ )
187
+ right_hand_joints = right_hand_joints.reshape(1, 7)
188
+ pose_data["right_hand_joints"] = right_hand_joints
189
+
190
+ return pack_pose_message(pose_data, topic="pose", version=4)
191
+
192
+
193
+ def get_action_field(action_dict: dict, key: str):
194
+ """Get action field from dict, checking both with and without 'action.' prefix."""
195
+ value = action_dict.get(key)
196
+ if value is not None:
197
+ return value
198
+ value = action_dict.get(f"action.{key}")
199
+ if value is not None:
200
+ return value
201
+ raise AssertionError(
202
+ f"Required action field '{key}' (or 'action.{key}') not found in processed_action. "
203
+ f"Available keys: {list(action_dict.keys())}"
204
+ )
205
+
206
+
207
+ # ---------------------------------------------------------------------------
208
+ # Observation / inference helpers
209
+ # ---------------------------------------------------------------------------
210
+
211
+
212
+ def prepare_observation_from_sensors(
213
+ camera_subscriber,
214
+ state_subscriber,
215
+ robot_model,
216
+ language_prompt: str,
217
+ log_errors: bool = False,
218
+ ):
219
+ """Read sensors and prepare observation for the VLA policy.
220
+
221
+ Returns:
222
+ observation dict, or None if sensor data not yet available.
223
+ """
224
+ camera_msg = camera_subscriber.read()
225
+ if camera_msg is None:
226
+ if log_errors:
227
+ print("[DEBUG] prepare_observation: waiting for camera msg..", flush=True)
228
+ return None
229
+
230
+ state_msg = state_subscriber.get_msg()
231
+ if state_msg is None:
232
+ if log_errors:
233
+ print("[DEBUG] prepare_observation: waiting for state msg..", flush=True)
234
+ return None
235
+
236
+ cam_img = camera_msg["images"]["ego_view"]
237
+
238
+ # Copy index finger data to middle finger (hardware coupling)
239
+ state_msg["left_hand_q"][5] = state_msg["left_hand_q"][3]
240
+ state_msg["left_hand_q"][6] = state_msg["left_hand_q"][4]
241
+
242
+ qpos = robot_model.get_configuration_from_actuated_joints(
243
+ body_actuated_joint_values=state_msg["body_q"],
244
+ left_hand_actuated_joint_values=state_msg["left_hand_q"],
245
+ right_hand_actuated_joint_values=state_msg["right_hand_q"],
246
+ )
247
+
248
+ video = {"ego_view": cam_img[np.newaxis, np.newaxis]}
249
+ if "left_wrist" in camera_msg["images"]:
250
+ video["left_wrist"] = camera_msg["images"]["left_wrist"][np.newaxis, np.newaxis]
251
+ if "right_wrist" in camera_msg["images"]:
252
+ video["wrist_view"] = camera_msg["images"]["right_wrist"][np.newaxis, np.newaxis]
253
+
254
+ observation = {
255
+ "video": video,
256
+ "state": {},
257
+ "language": {
258
+ "annotation.human.task_description": [[language_prompt]],
259
+ },
260
+ "q": np.asarray(qpos, dtype=np.float32)[np.newaxis, np.newaxis],
261
+ "timestamps": camera_msg["timestamps"]["ego_view"],
262
+ }
263
+
264
+ observation = prepare_observation_for_eval(robot_model, observation)
265
+
266
+ # Projected gravity for Sonic latent embodiment
267
+ assert "base_quat" in state_msg, "base_quat not found in state_msg"
268
+ base_quat = np.asarray(state_msg["base_quat"], dtype=np.float64)
269
+ assert base_quat.shape == (4,), "base_quat must have shape (4,)"
270
+ projected_gravity = compute_projected_gravity(base_quat)
271
+ observation["state"]["projected_gravity"] = np.asarray(
272
+ projected_gravity, dtype=np.float32
273
+ )[np.newaxis, np.newaxis]
274
+
275
+ return observation
276
+
277
+
278
+ def run_policy_inference_and_process(policy, observation, robot_model):
279
+ """Run policy inference via Isaac-GR00T PolicyClient and process results.
280
+
281
+ Returns:
282
+ processed_action dict or None on error.
283
+ """
284
+ try:
285
+ action, _info = policy.get_action(observation)
286
+
287
+ action.pop("task_progress", None)
288
+ action.pop("action.task_progress", None)
289
+
290
+ motion_key = "motion_token" if "motion_token" in action else "action.motion_token"
291
+ if np.abs(action[motion_key]).max() > 1.25:
292
+ print(
293
+ f"[Warning] action['{motion_key}'] max "
294
+ f"({np.abs(action[motion_key]).max():.4f}) > 1.25. "
295
+ "Exceeds action bound, skipping."
296
+ )
297
+ return None
298
+
299
+ processed_action = concat_action(robot_model, action)
300
+ return processed_action
301
+ except Exception as e:
302
+ print(f"Error in inference: {e}")
303
+ import traceback
304
+
305
+ traceback.print_exc()
306
+ return None
307
+
308
+
309
+ def _inference_worker_loop(
310
+ inference_queue: queue.Queue,
311
+ result_queue: queue.Queue,
312
+ stop_event: threading.Event,
313
+ busy_event: threading.Event,
314
+ prepare_obs_fn,
315
+ inference_fn,
316
+ ):
317
+ """Persistent worker thread for async inference."""
318
+ while not stop_event.is_set():
319
+ try:
320
+ try:
321
+ inference_queue.get(timeout=0.1)
322
+ except queue.Empty:
323
+ continue
324
+
325
+ busy_event.set()
326
+ try:
327
+ observation = prepare_obs_fn()
328
+ if observation is None:
329
+ print("[DEBUG] Worker thread: Observation is None, skipping", flush=True)
330
+ continue
331
+
332
+ inference_start_time = time.monotonic()
333
+ processed_action = inference_fn(observation)
334
+
335
+ if processed_action is not None:
336
+ try:
337
+ result_queue.put_nowait((processed_action, inference_start_time))
338
+ except queue.Full:
339
+ try:
340
+ result_queue.get_nowait()
341
+ result_queue.put_nowait((processed_action, inference_start_time))
342
+ except queue.Empty:
343
+ result_queue.put_nowait((processed_action, inference_start_time))
344
+ finally:
345
+ busy_event.clear()
346
+ except Exception as e:
347
+ print(f"Error in inference worker thread: {e}")
348
+ import traceback
349
+
350
+ traceback.print_exc()
351
+
352
+
353
+ # ---------------------------------------------------------------------------
354
+ # Main
355
+ # ---------------------------------------------------------------------------
356
+
357
+
358
+ def _compute_closed_hand_joints(side: str) -> np.ndarray:
359
+ """Compute closed hand joint positions using G1GripperInverseKinematicsSolver."""
360
+ side_str = "left" if side.upper() == "L" else "right"
361
+ solver = G1GripperInverseKinematicsSolver(side=side_str)
362
+ return solver._get_middle_close_q_desired().astype(np.float32)
363
+
364
+
365
+ def main(config: InferenceConfig):
366
+ pause_loop = True
367
+
368
+ robot_model = instantiate_g1_robot_model(waist_location="lower_and_upper_body")
369
+
370
+ # Isaac-GR00T PolicyClient
371
+ from gr00t.policy.server_client import PolicyClient
372
+
373
+ n1_policy = PolicyClient(host=config.host, port=config.port)
374
+
375
+ print(f"Connecting to PolicyServer at {config.host}:{config.port}...")
376
+ if n1_policy.ping():
377
+ print_green("PolicyServer is reachable.")
378
+ else:
379
+ print("WARNING: PolicyServer not reachable. Inference will fail until server is up.")
380
+
381
+ state_subscriber = ZMQStateSubscriber(
382
+ host=config.state_zmq_host,
383
+ port=config.state_zmq_port,
384
+ )
385
+
386
+ camera_subscriber = ComposedCameraClientSensor(
387
+ server_ip=config.camera_host, port=config.camera_port
388
+ )
389
+
390
+ zmq_context = zmq.Context()
391
+ zmq_socket = zmq_context.socket(zmq.PUB)
392
+ zmq_socket.bind(f"tcp://{config.action_zmq_host}:{config.action_zmq_port}")
393
+ time.sleep(0.1)
394
+ print_green(
395
+ f"ZMQ action socket bound to tcp://{config.action_zmq_host}:{config.action_zmq_port}"
396
+ )
397
+ print_green(f"Using embodiment tag: {config.embodiment_tag}")
398
+
399
+ keyboard_listener = ZMQKeyboardSubscriber(
400
+ port=config.keyboard_zmq_port, host=config.keyboard_zmq_host
401
+ )
402
+
403
+ telemetry = Telemetry(window_size=100)
404
+
405
+ loop_rate = config.action_publish_rate
406
+ loop_period = 1.0 / loop_rate
407
+
408
+ # Track C++ control loop state
409
+ cpp_loop_running = False
410
+ cpp_mode = "OFF" # "OFF", "PLANNER", or "POSE"
411
+
412
+ # Track initial pose hand states
413
+ initial_pose_left_hand_closed = False
414
+ initial_pose_right_hand_closed = False
415
+
416
+ def publish_initial_pose():
417
+ """Publish initial pose command to move robot to starting position."""
418
+ print("Moving to initial pose")
419
+ left_hand = (
420
+ _compute_closed_hand_joints("L")
421
+ if initial_pose_left_hand_closed
422
+ else np.zeros(7, dtype=np.float32)
423
+ )
424
+ right_hand = (
425
+ _compute_closed_hand_joints("R")
426
+ if initial_pose_right_hand_closed
427
+ else np.zeros(7, dtype=np.float32)
428
+ )
429
+ zmq_message = pack_latent_action_message(
430
+ motion_token=LATENT_INITIAL_MOTION_TOKEN,
431
+ frame_index=np.array([0], dtype=np.int64),
432
+ left_hand_joints=left_hand,
433
+ right_hand_joints=right_hand,
434
+ )
435
+ zmq_socket.send(zmq_message)
436
+ print_green("Sent latent initial pose via ZMQ")
437
+ time.sleep(1.0)
438
+ print("Initial pose done.")
439
+
440
+ def blend_to_initial_pose(duration_s: float) -> bool:
441
+ """Smoothly interpolate from the last sent motion token to the initial pose.
442
+
443
+ Linearly blends over ``duration_s`` seconds at the action publish rate,
444
+ sending intermediate tokens each loop iteration. Returns True if blend
445
+ was performed, False if skipped (no previous token available).
446
+ """
447
+ nonlocal last_sent_motion_token
448
+ if last_sent_motion_token is None:
449
+ print("No previous motion token — snapping to initial pose instead.")
450
+ publish_initial_pose()
451
+ return False
452
+
453
+ start_token = last_sent_motion_token.copy()
454
+ target_token = LATENT_INITIAL_MOTION_TOKEN.copy()
455
+ num_steps = max(1, round(config.action_publish_rate * duration_s))
456
+ step_period = 1.0 / config.action_publish_rate
457
+
458
+ left_hand = (
459
+ _compute_closed_hand_joints("L")
460
+ if initial_pose_left_hand_closed
461
+ else np.zeros(7, dtype=np.float32)
462
+ )
463
+ right_hand = (
464
+ _compute_closed_hand_joints("R")
465
+ if initial_pose_right_hand_closed
466
+ else np.zeros(7, dtype=np.float32)
467
+ )
468
+
469
+ print(
470
+ f"Blending to initial pose over {duration_s:.2f}s "
471
+ f"({num_steps} steps at {config.action_publish_rate} Hz)"
472
+ )
473
+
474
+ for step in range(num_steps):
475
+ t_step_start = time.monotonic()
476
+ alpha = (step + 1) / num_steps
477
+ blended_token = ((1.0 - alpha) * start_token + alpha * target_token).astype(
478
+ np.float32
479
+ )
480
+ zmq_message = pack_latent_action_message(
481
+ motion_token=blended_token,
482
+ frame_index=np.array([0], dtype=np.int64),
483
+ left_hand_joints=left_hand,
484
+ right_hand_joints=right_hand,
485
+ )
486
+ zmq_socket.send(zmq_message)
487
+ last_sent_motion_token = blended_token.copy()
488
+
489
+ elapsed = time.monotonic() - t_step_start
490
+ remaining = step_period - elapsed
491
+ if remaining > 0:
492
+ time.sleep(remaining)
493
+
494
+ print_green("Initial pose blend complete.")
495
+ return True
496
+
497
+ def send_cpp_control_command(start: bool, planner: bool = False):
498
+ """Send C++ control loop start/stop commands via ZMQ."""
499
+ nonlocal cpp_loop_running, cpp_mode
500
+ try:
501
+ cmd_msg = build_command_message(start=start, stop=not start, planner=planner)
502
+ zmq_socket.send(cmd_msg)
503
+ time.sleep(0.01)
504
+ action_str = "start" if start else "stop"
505
+ mode_str = "planner" if planner else "pose"
506
+ cpp_loop_running = start
507
+ if start:
508
+ cpp_mode = "PLANNER" if planner else "POSE"
509
+ else:
510
+ cpp_mode = "OFF"
511
+ print_green(f"Sent ZMQ command: {action_str} control loop ({mode_str} mode)")
512
+ return True
513
+ except Exception as e:
514
+ action_str = "start" if start else "stop"
515
+ print(f"Warning: Failed to send {action_str} command message: {e}")
516
+ return False
517
+
518
+ # Async inference state
519
+ cached_action_chunk = None
520
+ action_chunk_index = 0
521
+ last_inference_time = 0.0
522
+ inference_interval = 1.0 / config.rate
523
+
524
+ zmq_frame_counter = 0
525
+ last_sent_motion_token: np.ndarray | None = None
526
+
527
+ PROMPT_MSG_PREFIX = "prompt:"
528
+
529
+ def check_keyboard_input():
530
+ nonlocal pause_loop, cpp_loop_running, cpp_mode
531
+ nonlocal initial_pose_left_hand_closed, initial_pose_right_hand_closed
532
+ nonlocal cached_action_chunk, action_chunk_index, last_inference_time
533
+ nonlocal zmq_frame_counter, last_sent_motion_token
534
+
535
+ key = keyboard_listener.read_msg()
536
+ if key is None:
537
+ return
538
+
539
+ if key.startswith(PROMPT_MSG_PREFIX):
540
+ new_prompt = key[len(PROMPT_MSG_PREFIX):]
541
+ if new_prompt:
542
+ old_prompt = language_prompt_ref[0]
543
+ language_prompt_ref[0] = new_prompt
544
+ print_green(f'Inference prompt changed: "{old_prompt}" -> "{new_prompt}"')
545
+ else:
546
+ print("Received empty prompt change -- ignoring.")
547
+ return
548
+
549
+ if key == "c":
550
+ print("Keyboard: 'c' (start recording -- handled by data exporter)")
551
+ elif key == "s":
552
+ print("Keyboard: 's' (stop recording success -- handled by data exporter)")
553
+ elif key == "f":
554
+ print("Keyboard: 'f' (stop recording failure -- handled by data exporter)")
555
+ elif key == "i":
556
+ if cpp_loop_running and cpp_mode == "PLANNER":
557
+ if send_cpp_control_command(start=True, planner=False):
558
+ print("Switched to POSE mode (from PLANNER mode)")
559
+ else:
560
+ print("Warning: Failed to switch to POSE mode")
561
+ elif not cpp_loop_running:
562
+ print("Note: C++ loop not running - press 'k' to start")
563
+
564
+ pause_loop = True
565
+ if config.initial_pose_blend_duration > 0 and last_sent_motion_token is not None:
566
+ blend_to_initial_pose(config.initial_pose_blend_duration)
567
+ else:
568
+ publish_initial_pose()
569
+
570
+ zmq_frame_counter = 0
571
+ cached_action_chunk = None
572
+ action_chunk_index = 0
573
+ print("Cleared cached action chunk, reset frame counter")
574
+ elif key == "p":
575
+ pause_loop = not pause_loop
576
+ print(f"{'Paused' if pause_loop else 'Resumed'} policy loop")
577
+ if pause_loop:
578
+ print("Policy loop paused (C++ loop still running - press 'k' to stop)")
579
+ else:
580
+ print("Policy loop resumed")
581
+ elif key == "k":
582
+ if cpp_loop_running:
583
+ current_planner = cpp_mode == "PLANNER"
584
+ print(f"Stopping C++ control loop (from {cpp_mode} mode)...")
585
+ if send_cpp_control_command(start=False, planner=current_planner):
586
+ print("Stopped C++ control loop")
587
+ else:
588
+ print("Starting C++ control loop in PLANNER mode...")
589
+ if send_cpp_control_command(start=True, planner=True):
590
+ print("Started C++ control loop in PLANNER mode")
591
+ print("Press 'i' to send initial pose and switch to POSE mode")
592
+ if pause_loop:
593
+ print("Note: Policy loop is paused - press 'p' to resume")
594
+ elif key == "[":
595
+ initial_pose_left_hand_closed = not initial_pose_left_hand_closed
596
+ print(
597
+ f"Initial pose left hand: {'closed' if initial_pose_left_hand_closed else 'open'}"
598
+ )
599
+ elif key == "]":
600
+ initial_pose_right_hand_closed = not initial_pose_right_hand_closed
601
+ print(
602
+ f"Initial pose right hand: "
603
+ f"{'closed' if initial_pose_right_hand_closed else 'open'}"
604
+ )
605
+
606
+ # Mutable prompt container (single-writer from keyboard, single-reader from inference)
607
+ language_prompt_ref: list[str] = [config.prompt]
608
+ print(f"Starting the policy loop with language prompt: {language_prompt_ref[0]}")
609
+
610
+ inference_queue = queue.Queue(maxsize=1)
611
+ result_queue = queue.Queue(maxsize=1)
612
+ inference_stop_event = threading.Event()
613
+ inference_busy_event = threading.Event()
614
+
615
+ inference_worker_thread = threading.Thread(
616
+ target=_inference_worker_loop,
617
+ args=(
618
+ inference_queue,
619
+ result_queue,
620
+ inference_stop_event,
621
+ inference_busy_event,
622
+ lambda: prepare_observation_from_sensors(
623
+ camera_subscriber=camera_subscriber,
624
+ state_subscriber=state_subscriber,
625
+ robot_model=robot_model,
626
+ language_prompt=language_prompt_ref[0],
627
+ log_errors=True,
628
+ ),
629
+ lambda obs: run_policy_inference_and_process(
630
+ policy=n1_policy,
631
+ observation=obs,
632
+ robot_model=robot_model,
633
+ ),
634
+ ),
635
+ daemon=True,
636
+ )
637
+ inference_worker_thread.start()
638
+
639
+ try:
640
+ while True:
641
+ t_start = time.monotonic()
642
+ check_keyboard_input()
643
+
644
+ # Consume result first so last_inference_time is fresh before trigger check
645
+ try:
646
+ processed_action, inference_start_time = result_queue.get_nowait()
647
+ inference_delay = time.monotonic() - inference_start_time
648
+ action_chunk_index = calculate_latency_compensated_index(
649
+ inference_delay, config.action_publish_rate, config.action_horizon
650
+ )
651
+ cached_action_chunk = processed_action
652
+ last_inference_time = time.monotonic()
653
+ print_green(
654
+ f'New action chunk (prompt: "{language_prompt_ref[0]}", '
655
+ f"latency: {inference_delay:.3f}s)"
656
+ )
657
+ except queue.Empty:
658
+ pass
659
+
660
+ worker_is_busy = inference_busy_event.is_set()
661
+ should_start = should_trigger_new_inference(
662
+ cached_chunk_exists=(cached_action_chunk is not None),
663
+ inference_thread_running=worker_is_busy,
664
+ time_since_last_inference=(time.monotonic() - last_inference_time),
665
+ inference_interval=inference_interval,
666
+ )
667
+
668
+ if should_start:
669
+ try:
670
+ inference_queue.put_nowait(None)
671
+ except queue.Full:
672
+ pass
673
+
674
+ if pause_loop:
675
+ print("Pausing...", end="", flush=True)
676
+ time.sleep(0.2)
677
+ print(".", end="", flush=True)
678
+ continue
679
+
680
+ with telemetry.timer("total_loop"):
681
+ if cached_action_chunk is None:
682
+ print("[DEBUG] No cached chunk yet, waiting...", flush=True)
683
+ _sleep_remaining(t_start, loop_period)
684
+ continue
685
+
686
+ processed_action = cached_action_chunk
687
+
688
+ if processed_action is None or not processed_action:
689
+ print("[DEBUG] processed_action is None or empty, skipping", flush=True)
690
+ else:
691
+ motion_token = np.asarray(
692
+ get_action_field(processed_action, "motion_token"),
693
+ dtype=np.float32,
694
+ )
695
+ left_hand_joints = np.asarray(
696
+ get_action_field(processed_action, "left_hand_joints"),
697
+ dtype=np.float32,
698
+ )
699
+ right_hand_joints = np.asarray(
700
+ get_action_field(processed_action, "right_hand_joints"),
701
+ dtype=np.float32,
702
+ )
703
+
704
+ # Action arrays arrive as (B, T, D) from the model.
705
+ # Squeeze batch dim to get (T, D), then index by time step.
706
+ if motion_token.ndim == 3:
707
+ motion_token = motion_token[0]
708
+ if left_hand_joints.ndim == 3:
709
+ left_hand_joints = left_hand_joints[0]
710
+ if right_hand_joints.ndim == 3:
711
+ right_hand_joints = right_hand_joints[0]
712
+
713
+ horizon = motion_token.shape[0] if motion_token.ndim == 2 else 1
714
+ current_idx = min(action_chunk_index, horizon - 1)
715
+
716
+ if motion_token.ndim == 2:
717
+ motion_token = motion_token[current_idx]
718
+ if left_hand_joints.ndim == 2:
719
+ left_hand_joints = left_hand_joints[current_idx]
720
+ if right_hand_joints.ndim == 2:
721
+ right_hand_joints = right_hand_joints[current_idx]
722
+
723
+ frame_index = np.array([zmq_frame_counter], dtype=np.int64)
724
+ zmq_frame_counter += 1
725
+
726
+ zmq_message = pack_latent_action_message(
727
+ motion_token,
728
+ frame_index,
729
+ left_hand_joints=left_hand_joints,
730
+ right_hand_joints=right_hand_joints,
731
+ )
732
+ zmq_socket.send(zmq_message)
733
+ last_sent_motion_token = motion_token.copy()
734
+ if zmq_frame_counter % 50 == 0:
735
+ print_green(
736
+ f"ZMQ: Sent latent action - "
737
+ f"frame: {frame_index[0]}, "
738
+ f"token shape: {motion_token.shape}"
739
+ )
740
+
741
+ action_chunk_index = min(action_chunk_index + 1, config.action_horizon - 1)
742
+
743
+ end_time = time.monotonic()
744
+
745
+ if config.verbose_timing:
746
+ telemetry.log_timing_info(context="VLA Inference Loop", threshold=0.0)
747
+ elif (end_time - t_start) > (1 / config.rate):
748
+ telemetry.log_timing_info(
749
+ context="VLA Inference Loop Missed", threshold=0.001
750
+ )
751
+
752
+ _sleep_remaining(t_start, loop_period)
753
+
754
+ except KeyboardInterrupt:
755
+ print("VLA inference loop terminated by user")
756
+
757
+ finally:
758
+ inference_stop_event.set()
759
+ inference_worker_thread.join(timeout=1.0)
760
+ zmq_socket.close()
761
+ zmq_context.term()
762
+ state_subscriber.close()
763
+ keyboard_listener.close()
764
+ print("Shutdown complete.")
765
+
766
+
767
+ def _sleep_remaining(t_start: float, loop_period: float):
768
+ """Sleep for the remainder of the loop period."""
769
+ elapsed = time.monotonic() - t_start
770
+ remaining = loop_period - elapsed
771
+ if remaining > 0:
772
+ time.sleep(remaining)
773
+
774
+
775
+ if __name__ == "__main__":
776
+ config = tyro.cli(InferenceConfig)
777
+ main(config)
GR00T-WholeBodyControl/gear_sonic/scripts/run_vla_inference_dex1_head.py ADDED
@@ -0,0 +1,697 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Run standard GR00T N1.7 inference for SONIC + Dex1 + a two-axis head.
2
+
3
+ This client is intentionally *not* an RTC client. Each policy request calls
4
+ ``PolicyClient.get_action(observation)`` with no previous action chunk and no
5
+ RTC options. It retains the official GR00T-WholeBodyControl asynchronous
6
+ inference cadence and latency-compensated chunk index.
7
+
8
+ Checkpoint contract used by this entry point:
9
+
10
+ * video: ``ego_view``, ``left_wrist``, ``right_wrist`` as separate tensors;
11
+ * state: G1 body groups, two scalar grippers, measured yaw/pitch, gravity;
12
+ * action: 64-D SONIC token, two scalar grippers, 2-D absolute head target;
13
+ * horizon: 40 steps.
14
+
15
+ The program starts paused. In the terminal, enter ``k`` (start SONIC planner),
16
+ ``i`` (initialize and switch to pose mode), then ``p`` (run the policy).
17
+ """
18
+
19
+ # ruff: noqa: E402 -- remove ROS Python 3.10 paths before importing Pinocchio users
20
+
21
+ from __future__ import annotations
22
+
23
+ from dataclasses import dataclass
24
+ import queue
25
+ import sys
26
+ import threading
27
+ import time
28
+ import traceback
29
+ from typing import Any
30
+
31
+ # ROS Humble can inject incompatible Python 3.10 packages into a Python 3.12
32
+ # inference environment before cmeel/Pinocchio is imported.
33
+ sys.path[:] = [
34
+ path
35
+ for path in sys.path
36
+ if not ("/opt/ros/humble/" in path and "python3.10" in path)
37
+ ]
38
+
39
+ import numpy as np
40
+ import tyro
41
+ import zmq
42
+
43
+ from gear_sonic.camera.composed_camera import ComposedCameraClientSensor
44
+ from gear_sonic.data.robot_model.instantiation.g1 import instantiate_g1_robot_model
45
+ from gear_sonic.scripts.run_vla_inference import pack_latent_action_message
46
+ from gear_sonic.utils.data_collection.keyboard_subscriber import (
47
+ DEFAULT_ZMQ_KEYBOARD_PORT,
48
+ ZMQKeyboardSubscriber,
49
+ )
50
+ from gear_sonic.utils.data_collection.transforms import compute_projected_gravity
51
+ from gear_sonic.utils.data_collection.zmq_state_subscriber import ZMQStateSubscriber
52
+ from gear_sonic.utils.inference.dex1_head import (
53
+ Dex1CommandFilter,
54
+ HeadCommandFilter,
55
+ LatestMsgpackSubscriber,
56
+ pack_gripper_command,
57
+ pack_head_command,
58
+ parse_dex1_state,
59
+ parse_head_state,
60
+ validate_action_chunk,
61
+ )
62
+ from gear_sonic.utils.inference.initial_poses import LATENT_INITIAL_MOTION_TOKEN
63
+ from gear_sonic.utils.inference.vla_utils import (
64
+ calculate_latency_compensated_index,
65
+ prepare_observation_for_eval,
66
+ should_trigger_new_inference,
67
+ )
68
+ from gear_sonic.utils.teleop.zmq.zmq_planner_sender import build_command_message
69
+
70
+
71
+ BODY_STATE_KEYS = ("left_leg", "right_leg", "waist", "left_arm", "right_arm")
72
+ EXPECTED_STATE_DIMS = {
73
+ "left_leg": 6,
74
+ "right_leg": 6,
75
+ "waist": 3,
76
+ "left_arm": 7,
77
+ "right_arm": 7,
78
+ "left_gripper": 1,
79
+ "right_gripper": 1,
80
+ "head_joints": 2,
81
+ "projected_gravity": 3,
82
+ }
83
+ EXPECTED_VIDEO_KEYS = ("ego_view", "left_wrist", "right_wrist")
84
+
85
+
86
+ @dataclass
87
+ class InferenceConfig:
88
+ """Command-line configuration for the real-robot policy client."""
89
+
90
+ host: str = "localhost"
91
+ """GR00T PolicyServer host."""
92
+
93
+ port: int = 5550
94
+ """GR00T PolicyServer port."""
95
+
96
+ policy_timeout_ms: int = 30000
97
+ """Policy request timeout in milliseconds."""
98
+
99
+ embodiment_tag: str = "unitree_g1_sonic"
100
+ """Expected checkpoint embodiment (logged as a deployment assertion)."""
101
+
102
+ prompt: str = "Pick up the bottle and put it in the box"
103
+ """Initial language instruction."""
104
+
105
+ action_publish_rate: int = 50
106
+ """SONIC/Dex1/head command rate in Hz."""
107
+
108
+ action_horizon: int = 40
109
+ """Checkpoint action horizon."""
110
+
111
+ rate: float = 2.5
112
+ """Minimum completed-request cadence in Hz, matching the official client."""
113
+
114
+ camera_host: str = "192.168.123.164"
115
+ camera_port: int = 5555
116
+ state_zmq_host: str = "localhost"
117
+ state_zmq_port: int = 5557
118
+ hand_state_host: str = "192.168.123.164"
119
+ hand_state_port: int = 5559
120
+ head_state_host: str = "192.168.123.164"
121
+ head_state_port: int = 5561
122
+
123
+ output_bind_host: str = "0.0.0.0"
124
+ action_zmq_port: int = 5556
125
+ hand_command_port: int = 5558
126
+ head_command_port: int = 5560
127
+
128
+ use_zmq_keyboard: bool = False
129
+ """Read commands from the legacy ZMQ keyboard publisher instead of stdin."""
130
+
131
+ keyboard_zmq_host: str = "localhost"
132
+ keyboard_zmq_port: int = DEFAULT_ZMQ_KEYBOARD_PORT
133
+
134
+ state_timeout: float = 0.5
135
+ """Maximum age of Dex1/head feedback in seconds."""
136
+
137
+ max_gripper_step: float = 0.08
138
+ head_yaw_limits: tuple[float, float] = (-1.2, 1.2)
139
+ head_pitch_limits: tuple[float, float] = (-0.6, 0.6)
140
+ max_yaw_step: float = 0.08
141
+ max_pitch_step: float = 0.06
142
+
143
+ dry_run: bool = False
144
+ """Run sensing and inference but never publish hardware commands."""
145
+
146
+ debug: bool = False
147
+ verbose_timing: bool = False
148
+
149
+
150
+ def _endpoint(host: str, port: int) -> str:
151
+ return f"tcp://{host}:{port}"
152
+
153
+
154
+ def _green(message: str) -> None:
155
+ print(f"\033[92m{message}\033[0m", flush=True)
156
+
157
+
158
+ class StdinCommandReader:
159
+ """Read terminal commands on a daemon thread without blocking the control loop."""
160
+
161
+ def __init__(self) -> None:
162
+ self._commands: queue.Queue[str] = queue.Queue()
163
+ self._thread = threading.Thread(target=self._read_loop, daemon=True)
164
+ self._thread.start()
165
+
166
+ def _read_loop(self) -> None:
167
+ while True:
168
+ try:
169
+ value = input().strip()
170
+ except EOFError:
171
+ return
172
+ if value:
173
+ self._commands.put(value)
174
+
175
+ def read_msg(self) -> str | None:
176
+ try:
177
+ return self._commands.get_nowait()
178
+ except queue.Empty:
179
+ return None
180
+
181
+ def close(self) -> None:
182
+ return
183
+
184
+
185
+ class DiagnosticLogger:
186
+ def __init__(self, enabled: bool, interval: float = 1.0):
187
+ self.enabled = enabled
188
+ self.interval = interval
189
+ self._last: dict[str, float] = {}
190
+
191
+ def log(self, stage: str, message: str, *, force: bool = False) -> None:
192
+ if not self.enabled:
193
+ return
194
+ now = time.monotonic()
195
+ if not force and now - self._last.get(stage, float("-inf")) < self.interval:
196
+ return
197
+ self._last[stage] = now
198
+ print(f"[debug][{stage}] {message}", flush=True)
199
+
200
+ def exception(self, stage: str, error: Exception) -> None:
201
+ self.log(stage, f"{type(error).__name__}: {error}", force=True)
202
+ if self.enabled:
203
+ traceback.print_exc()
204
+
205
+
206
+ def _validate_image(name: str, image: Any) -> np.ndarray:
207
+ value = np.asarray(image)
208
+ if value.dtype != np.uint8 or value.ndim != 3 or value.shape[-1] != 3:
209
+ raise ValueError(
210
+ f"Camera {name!r} must be uint8 HxWx3, got dtype={value.dtype}, shape={value.shape}"
211
+ )
212
+ return np.ascontiguousarray(value)
213
+
214
+
215
+ def validate_observation_schema(observation: dict[str, Any]) -> None:
216
+ """Fail locally before a malformed observation crosses the policy network."""
217
+ if set(observation.get("video", {})) != set(EXPECTED_VIDEO_KEYS):
218
+ raise ValueError(
219
+ f"Video keys must be exactly {EXPECTED_VIDEO_KEYS}, got "
220
+ f"{sorted(observation.get('video', {}))}"
221
+ )
222
+ for key in EXPECTED_VIDEO_KEYS:
223
+ value = observation["video"][key]
224
+ if value.dtype != np.uint8 or value.ndim != 5 or value.shape[:2] != (1, 1):
225
+ raise ValueError(f"video.{key} must be uint8 [1,1,H,W,3], got {value.shape}")
226
+
227
+ if set(observation.get("state", {})) != set(EXPECTED_STATE_DIMS):
228
+ raise ValueError(
229
+ f"State keys must be exactly {tuple(EXPECTED_STATE_DIMS)}, got "
230
+ f"{sorted(observation.get('state', {}))}"
231
+ )
232
+ for key, width in EXPECTED_STATE_DIMS.items():
233
+ value = observation["state"][key]
234
+ if value.dtype != np.float32 or value.shape != (1, 1, width):
235
+ raise ValueError(f"state.{key} must be float32 [1,1,{width}], got {value.shape}")
236
+
237
+ language = observation.get("language", {}).get("annotation.human.task_description")
238
+ if not isinstance(language, list) or len(language) != 1:
239
+ raise ValueError("Language prompt must have a single batch entry")
240
+
241
+
242
+ def prepare_observation(
243
+ camera_subscriber,
244
+ state_subscriber,
245
+ hand_subscriber: LatestMsgpackSubscriber,
246
+ head_subscriber: LatestMsgpackSubscriber,
247
+ robot_model,
248
+ language_prompt: str,
249
+ state_timeout: float,
250
+ diagnostics: DiagnosticLogger | None = None,
251
+ ) -> dict[str, Any] | None:
252
+ """Build the exact observation schema saved in checkpoint-20000."""
253
+ camera_msg = camera_subscriber.read()
254
+ state_msg = state_subscriber.get_msg()
255
+ hand_msg = hand_subscriber.read()
256
+ head_msg = head_subscriber.read()
257
+ missing = [
258
+ name
259
+ for name, value in (
260
+ ("camera", camera_msg),
261
+ ("SONIC state", state_msg),
262
+ ("Dex1 state", hand_msg),
263
+ ("head state", head_msg),
264
+ )
265
+ if value is None
266
+ ]
267
+ if missing:
268
+ if diagnostics:
269
+ diagnostics.log("observation", f"waiting for {missing}")
270
+ return None
271
+ if not hand_subscriber.is_fresh(state_timeout):
272
+ if diagnostics:
273
+ diagnostics.log("observation", "Dex1 feedback is stale")
274
+ return None
275
+ if not head_subscriber.is_fresh(state_timeout):
276
+ if diagnostics:
277
+ diagnostics.log("observation", "head feedback is stale")
278
+ return None
279
+
280
+ images = camera_msg.get("images", {})
281
+ missing_images = set(EXPECTED_VIDEO_KEYS).difference(images)
282
+ if missing_images:
283
+ raise KeyError(
284
+ f"Camera server is missing trained views {sorted(missing_images)}; "
285
+ f"available views: {sorted(images)}"
286
+ )
287
+
288
+ body_q = np.asarray(state_msg.get("body_q"), dtype=np.float32)
289
+ if body_q.shape != (29,) or not np.isfinite(body_q).all():
290
+ raise ValueError(f"body_q must be finite [29], got {body_q.shape}")
291
+ base_quat = np.asarray(state_msg.get("base_quat"), dtype=np.float64)
292
+ if base_quat.shape != (4,) or not np.isfinite(base_quat).all():
293
+ raise ValueError(f"base_quat must be finite [4], got {base_quat}")
294
+
295
+ left_gripper, right_gripper = parse_dex1_state(hand_msg)
296
+ measured_head = parse_head_state(head_msg)
297
+
298
+ # Hand joints are not checkpoint state modalities. Zeros are supplied only
299
+ # to satisfy RobotModel while splitting the 29 body joints into named groups.
300
+ unused_hand = np.zeros(7, dtype=np.float32)
301
+ qpos = robot_model.get_configuration_from_actuated_joints(
302
+ body_actuated_joint_values=body_q,
303
+ left_hand_actuated_joint_values=unused_hand,
304
+ right_hand_actuated_joint_values=unused_hand,
305
+ )
306
+ grouped = {"state": {}, "q": np.asarray(qpos, dtype=np.float32)[None, None]}
307
+ prepare_observation_for_eval(robot_model, grouped)
308
+
309
+ state = {
310
+ key: np.ascontiguousarray(grouped["state"][key], dtype=np.float32)
311
+ for key in BODY_STATE_KEYS
312
+ }
313
+ state["left_gripper"] = np.asarray(left_gripper, dtype=np.float32).reshape(1, 1, 1)
314
+ state["right_gripper"] = np.asarray(right_gripper, dtype=np.float32).reshape(1, 1, 1)
315
+ state["head_joints"] = measured_head.reshape(1, 1, 2)
316
+ state["projected_gravity"] = np.asarray(
317
+ compute_projected_gravity(base_quat), dtype=np.float32
318
+ ).reshape(1, 1, 3)
319
+
320
+ observation = {
321
+ "video": {
322
+ key: _validate_image(key, images[key])[None, None] for key in EXPECTED_VIDEO_KEYS
323
+ },
324
+ "state": state,
325
+ "language": {"annotation.human.task_description": [[language_prompt]]},
326
+ }
327
+ validate_observation_schema(observation)
328
+ if diagnostics:
329
+ diagnostics.log(
330
+ "observation",
331
+ "ready: "
332
+ f"video={{{', '.join(f'{k}: {v.shape}' for k, v in observation['video'].items())}}}, "
333
+ f"state={{{', '.join(f'{k}: {v.shape}' for k, v in state.items())}}}",
334
+ )
335
+ return observation
336
+
337
+
338
+ def _inference_worker_loop(
339
+ requests: queue.Queue,
340
+ results: queue.Queue,
341
+ stop_event: threading.Event,
342
+ busy_event: threading.Event,
343
+ prepare_observation_fn,
344
+ infer_fn,
345
+ action_horizon: int,
346
+ diagnostics: DiagnosticLogger,
347
+ ) -> None:
348
+ while not stop_event.is_set():
349
+ try:
350
+ try:
351
+ requests.get(timeout=0.1)
352
+ except queue.Empty:
353
+ continue
354
+ busy_event.set()
355
+ stage = "observation"
356
+ try:
357
+ observation = prepare_observation_fn()
358
+ if observation is None:
359
+ continue
360
+ stage = "policy"
361
+ started_at = time.monotonic()
362
+ # Standard GR00T call: deliberately no options and no previous chunk.
363
+ action, _info = infer_fn(observation)
364
+ stage = "action"
365
+ chunk = validate_action_chunk(action, action_horizon)
366
+ try:
367
+ results.put_nowait((chunk, started_at))
368
+ except queue.Full:
369
+ results.get_nowait()
370
+ results.put_nowait((chunk, started_at))
371
+ except Exception as error:
372
+ print(f"[inference][{stage}] rejected request: {error}", flush=True)
373
+ diagnostics.exception(stage, error)
374
+ finally:
375
+ busy_event.clear()
376
+ except Exception as error:
377
+ print(f"[inference worker] {error}", flush=True)
378
+ diagnostics.exception("worker", error)
379
+
380
+
381
+ def _validate_config(config: InferenceConfig) -> None:
382
+ if config.embodiment_tag.lower() != "unitree_g1_sonic":
383
+ raise ValueError("This checkpoint must use embodiment_tag=unitree_g1_sonic")
384
+ if config.action_horizon != 40:
385
+ raise ValueError("This checkpoint requires action_horizon=40")
386
+ if config.action_publish_rate <= 0 or config.rate <= 0:
387
+ raise ValueError("action_publish_rate and rate must be positive")
388
+ if config.state_timeout <= 0:
389
+ raise ValueError("state_timeout must be positive")
390
+
391
+
392
+ def _sleep_remaining(started_at: float, period: float) -> None:
393
+ remaining = period - (time.monotonic() - started_at)
394
+ if remaining > 0:
395
+ time.sleep(remaining)
396
+
397
+
398
+ def main(config: InferenceConfig) -> None:
399
+ _validate_config(config)
400
+ from gr00t.policy.server_client import PolicyClient
401
+
402
+ diagnostics = DiagnosticLogger(config.debug)
403
+ robot_model = instantiate_g1_robot_model(waist_location="lower_and_upper_body")
404
+ policy = PolicyClient(
405
+ host=config.host,
406
+ port=config.port,
407
+ timeout_ms=config.policy_timeout_ms,
408
+ )
409
+ print(f"Connecting to standard GR00T PolicyServer at {config.host}:{config.port}")
410
+ if not policy.ping():
411
+ print("WARNING: PolicyServer is not reachable yet; inference requests will fail")
412
+ else:
413
+ _green("PolicyServer is reachable")
414
+
415
+ camera = ComposedCameraClientSensor(server_ip=config.camera_host, port=config.camera_port)
416
+ robot_state = ZMQStateSubscriber(host=config.state_zmq_host, port=config.state_zmq_port)
417
+ context = zmq.Context()
418
+ hand_state = LatestMsgpackSubscriber(
419
+ context,
420
+ _endpoint(config.hand_state_host, config.hand_state_port),
421
+ parse_dex1_state,
422
+ )
423
+ head_state = LatestMsgpackSubscriber(
424
+ context,
425
+ _endpoint(config.head_state_host, config.head_state_port),
426
+ parse_head_state,
427
+ )
428
+ keyboard = (
429
+ ZMQKeyboardSubscriber(
430
+ host=config.keyboard_zmq_host,
431
+ port=config.keyboard_zmq_port,
432
+ )
433
+ if config.use_zmq_keyboard
434
+ else StdinCommandReader()
435
+ )
436
+
437
+ output_sockets: dict[str, zmq.Socket] = {}
438
+ if config.dry_run:
439
+ print("DRY RUN: hardware command publication is disabled")
440
+ else:
441
+ for name, port in (
442
+ ("sonic", config.action_zmq_port),
443
+ ("hand", config.hand_command_port),
444
+ ("head", config.head_command_port),
445
+ ):
446
+ socket = context.socket(zmq.PUB)
447
+ socket.setsockopt(zmq.SNDHWM, 2)
448
+ socket.bind(_endpoint(config.output_bind_host, port))
449
+ output_sockets[name] = socket
450
+ _green(f"{name} command socket bound on {config.output_bind_host}:{port}")
451
+ time.sleep(0.2)
452
+
453
+ paused = True
454
+ initialized = False
455
+ cpp_running = False
456
+ cpp_mode = "OFF"
457
+ prompt = [config.prompt]
458
+ cached_chunk: dict[str, np.ndarray] | None = None
459
+ chunk_index = 0
460
+ last_inference_time = 0.0
461
+ frame_counter = 0
462
+ gripper_filter = Dex1CommandFilter(max_step=config.max_gripper_step)
463
+ head_filter = HeadCommandFilter(
464
+ yaw_limits=config.head_yaw_limits,
465
+ pitch_limits=config.head_pitch_limits,
466
+ max_yaw_step=config.max_yaw_step,
467
+ max_pitch_step=config.max_pitch_step,
468
+ )
469
+
470
+ requests: queue.Queue = queue.Queue(maxsize=1)
471
+ results: queue.Queue = queue.Queue(maxsize=1)
472
+ stop_event = threading.Event()
473
+ busy_event = threading.Event()
474
+ worker = threading.Thread(
475
+ target=_inference_worker_loop,
476
+ args=(
477
+ requests,
478
+ results,
479
+ stop_event,
480
+ busy_event,
481
+ lambda: prepare_observation(
482
+ camera,
483
+ robot_state,
484
+ hand_state,
485
+ head_state,
486
+ robot_model,
487
+ prompt[0],
488
+ config.state_timeout,
489
+ diagnostics,
490
+ ),
491
+ policy.get_action,
492
+ config.action_horizon,
493
+ diagnostics,
494
+ ),
495
+ daemon=True,
496
+ )
497
+ worker.start()
498
+
499
+ def publish_targets(
500
+ motion_token: Any,
501
+ left_target: Any,
502
+ right_target: Any,
503
+ head_target: Any,
504
+ ) -> None:
505
+ nonlocal frame_counter
506
+ token = np.asarray(motion_token, dtype=np.float32).reshape(-1)
507
+ if token.shape != (64,) or not np.isfinite(token).all():
508
+ raise ValueError(f"motion token must be finite [64], got {token.shape}")
509
+ left, right = gripper_filter.update(left_target, right_target)
510
+ head = head_filter.update(head_target)
511
+ if config.dry_run:
512
+ if frame_counter % config.action_publish_rate == 0:
513
+ print(
514
+ f"[dry-run] frame={frame_counter} grippers=({left:.3f}, {right:.3f}) "
515
+ f"head=({head[0]:.3f}, {head[1]:.3f}) token_peak={np.abs(token).max():.3f}"
516
+ )
517
+ else:
518
+ timestamp_ns = time.time_ns()
519
+ output_sockets["sonic"].send(
520
+ pack_latent_action_message(
521
+ token,
522
+ np.asarray([frame_counter], dtype=np.int64),
523
+ )
524
+ )
525
+ output_sockets["hand"].send(pack_gripper_command(left, right, timestamp_ns))
526
+ output_sockets["head"].send(
527
+ pack_head_command(float(head[0]), float(head[1]), timestamp_ns)
528
+ )
529
+ frame_counter += 1
530
+
531
+ def send_cpp_command(start: bool, planner: bool) -> None:
532
+ nonlocal cpp_running, cpp_mode
533
+ if config.dry_run:
534
+ print(f"[dry-run] SONIC {'start' if start else 'stop'} command suppressed")
535
+ else:
536
+ output_sockets["sonic"].send(
537
+ build_command_message(start=start, stop=not start, planner=planner)
538
+ )
539
+ cpp_running = start
540
+ cpp_mode = "PLANNER" if start and planner else "POSE" if start else "OFF"
541
+ print(f"SONIC control: running={cpp_running}, mode={cpp_mode}", flush=True)
542
+
543
+ def initialize_pose() -> bool:
544
+ nonlocal initialized, cached_chunk, chunk_index, paused
545
+ if not config.dry_run and not cpp_running:
546
+ print("Cannot initialize: press k first to start SONIC in PLANNER mode")
547
+ return False
548
+ hand = hand_state.read()
549
+ head = head_state.read()
550
+ if hand is None or not hand_state.is_fresh(config.state_timeout):
551
+ print("Cannot initialize: no fresh Dex1 feedback")
552
+ return False
553
+ if head is None or not head_state.is_fresh(config.state_timeout):
554
+ print("Cannot initialize: no fresh head feedback")
555
+ return False
556
+ left, right = parse_dex1_state(hand)
557
+ measured_head = parse_head_state(head)
558
+ gripper_filter.reset(left, right)
559
+ head_filter.reset(measured_head)
560
+ publish_targets(LATENT_INITIAL_MOTION_TOKEN, left, right, measured_head)
561
+ send_cpp_command(True, planner=False)
562
+ paused = True
563
+ initialized = True
564
+ cached_chunk = None
565
+ chunk_index = 0
566
+ print("Initial SONIC token sent; measured gripper/head pose held; policy remains paused")
567
+ return True
568
+
569
+ def handle_command(command: str) -> None:
570
+ nonlocal paused, initialized, cached_chunk, chunk_index
571
+ normalized = command.strip()
572
+ if normalized.startswith("prompt:"):
573
+ normalized = "t " + normalized[len("prompt:") :]
574
+ if normalized.startswith("t ") and normalized[2:].strip():
575
+ prompt[0] = normalized[2:].strip()
576
+ paused = True
577
+ cached_chunk = None
578
+ chunk_index = 0
579
+ print(f'Prompt changed to "{prompt[0]}"; policy paused')
580
+ elif normalized == "k":
581
+ if cpp_running:
582
+ paused = True
583
+ initialized = False
584
+ send_cpp_command(False, planner=(cpp_mode == "PLANNER"))
585
+ else:
586
+ initialized = False
587
+ send_cpp_command(True, planner=True)
588
+ print("Press i after the robot is stable in PLANNER mode")
589
+ elif normalized == "i":
590
+ initialize_pose()
591
+ elif normalized == "p":
592
+ if not paused:
593
+ paused = True
594
+ print("Policy paused")
595
+ elif not config.dry_run and (not initialized or cpp_mode != "POSE"):
596
+ print("Cannot resume: complete k -> i first")
597
+ else:
598
+ cached_chunk = None
599
+ chunk_index = 0
600
+ paused = False
601
+ print("Policy resumed; requesting a fresh standard GR00T chunk")
602
+ elif normalized in {"help", "h", "?"}:
603
+ print(
604
+ "Commands: k=start/stop SONIC, i=initialize pose, "
605
+ "p=pause/resume, t <text>=prompt"
606
+ )
607
+ elif normalized:
608
+ print(f"Unknown command {normalized!r}; enter help")
609
+
610
+ print(f'Standard GR00T client ready with prompt: "{prompt[0]}"')
611
+ print("No RTC options are used. Camera views remain separate.")
612
+ print("Commands: k -> i -> p; enter 'help' for details. Ctrl-C stops publication.")
613
+
614
+ loop_period = 1.0 / config.action_publish_rate
615
+ inference_interval = 1.0 / config.rate
616
+ try:
617
+ while True:
618
+ tick_started = time.monotonic()
619
+ command = keyboard.read_msg()
620
+ if command:
621
+ handle_command(command)
622
+
623
+ try:
624
+ new_chunk, inference_started = results.get_nowait()
625
+ delay = time.monotonic() - inference_started
626
+ cached_chunk = new_chunk
627
+ chunk_index = calculate_latency_compensated_index(
628
+ delay,
629
+ config.action_publish_rate,
630
+ config.action_horizon,
631
+ )
632
+ last_inference_time = time.monotonic()
633
+ _green(
634
+ f"New standard action chunk: latency={delay:.3f}s, start_index={chunk_index}"
635
+ )
636
+ except queue.Empty:
637
+ pass
638
+
639
+ if not paused:
640
+ should_start = should_trigger_new_inference(
641
+ cached_chunk_exists=cached_chunk is not None,
642
+ inference_thread_running=busy_event.is_set(),
643
+ time_since_last_inference=time.monotonic() - last_inference_time,
644
+ inference_interval=inference_interval,
645
+ )
646
+ if should_start:
647
+ try:
648
+ requests.put_nowait(None)
649
+ except queue.Full:
650
+ pass
651
+
652
+ if cached_chunk is not None:
653
+ current = min(chunk_index, config.action_horizon - 1)
654
+ publish_targets(
655
+ cached_chunk["motion_token"][current],
656
+ cached_chunk["left_hand_joints"][current],
657
+ cached_chunk["right_hand_joints"][current],
658
+ cached_chunk["head_joints"][current],
659
+ )
660
+ chunk_index = min(chunk_index + 1, config.action_horizon - 1)
661
+
662
+ elapsed = time.monotonic() - tick_started
663
+ if config.verbose_timing and elapsed > loop_period:
664
+ print(f"Control tick overrun: {(elapsed - loop_period) * 1000:.1f} ms")
665
+ _sleep_remaining(tick_started, loop_period)
666
+ except KeyboardInterrupt:
667
+ print("Stopping real-robot GR00T client")
668
+ finally:
669
+ stop_event.set()
670
+ worker.join(timeout=1.0)
671
+ if not config.dry_run and cpp_running:
672
+ try:
673
+ output_sockets["sonic"].send(
674
+ build_command_message(
675
+ start=False,
676
+ stop=True,
677
+ planner=(cpp_mode == "PLANNER"),
678
+ )
679
+ )
680
+ time.sleep(0.05)
681
+ print("Sent SONIC stop command")
682
+ except Exception as error:
683
+ print(f"WARNING: failed to send SONIC stop command: {error}")
684
+ for socket in output_sockets.values():
685
+ socket.close(linger=0)
686
+ hand_state.close()
687
+ head_state.close()
688
+ robot_state.close()
689
+ camera.close()
690
+ keyboard.close()
691
+ policy.close()
692
+ context.term()
693
+ print("Shutdown complete")
694
+
695
+
696
+ if __name__ == "__main__":
697
+ main(tyro.cli(InferenceConfig))
GR00T-WholeBodyControl/gear_sonic/tests/test_dex1_head_inference.py ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Offline tests for the custom checkpoint/real-hardware boundary."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ import queue
7
+ import sys
8
+ import threading
9
+ import types
10
+ import unittest
11
+
12
+ import numpy as np
13
+
14
+ from gear_sonic.utils.inference.dex1_head import (
15
+ Dex1CommandFilter,
16
+ HeadCommandFilter,
17
+ parse_dex1_state,
18
+ validate_action_chunk,
19
+ )
20
+
21
+
22
+ def action_chunk(horizon: int = 40) -> dict:
23
+ return {
24
+ "motion_token": np.zeros((1, horizon, 64), dtype=np.float32),
25
+ "left_hand_joints": np.full((1, horizon, 1), 0.25, dtype=np.float32),
26
+ "right_hand_joints": np.full((1, horizon, 1), 0.75, dtype=np.float32),
27
+ "head_joints": np.zeros((1, horizon, 2), dtype=np.float32),
28
+ }
29
+
30
+
31
+ class ActionAndFilterTests(unittest.TestCase):
32
+ def test_checkpoint_action_contract_is_64_plus_1_plus_1_plus_2(self):
33
+ normalized = validate_action_chunk(action_chunk(), 40)
34
+ self.assertEqual(normalized["motion_token"].shape, (40, 64))
35
+ self.assertEqual(normalized["left_hand_joints"].shape, (40, 1))
36
+ self.assertEqual(normalized["right_hand_joints"].shape, (40, 1))
37
+ self.assertEqual(normalized["head_joints"].shape, (40, 2))
38
+
39
+ wrong = action_chunk()
40
+ wrong["left_hand_joints"] = np.zeros((1, 40, 7), dtype=np.float32)
41
+ with self.assertRaisesRegex(ValueError, r"\[T, 1\]"):
42
+ validate_action_chunk(wrong, 40)
43
+
44
+ def test_scalar_feedback_and_rate_limits(self):
45
+ self.assertEqual(
46
+ parse_dex1_state(
47
+ {"left_hand_joints": [0.2], "right_hand_joints": [0.8]}
48
+ ),
49
+ (0.2, 0.8),
50
+ )
51
+ gripper = Dex1CommandFilter(max_step=0.1, left_safe=0.2, right_safe=0.8)
52
+ left, right = gripper.update([1.0], [0.0])
53
+ self.assertAlmostEqual(left, 0.3)
54
+ self.assertAlmostEqual(right, 0.7)
55
+
56
+ head = HeadCommandFilter()
57
+ head.reset([0.0, 0.0])
58
+ np.testing.assert_allclose(head.update([2.0, -2.0]), [0.08, -0.06])
59
+
60
+
61
+ class ObservationAndStandardPolicyTests(unittest.TestCase):
62
+ @staticmethod
63
+ def install_stubs() -> None:
64
+ def register(name: str, **members) -> None:
65
+ module = types.ModuleType(name)
66
+ module.__dict__.update(members)
67
+ sys.modules[name] = module
68
+
69
+ register("tyro", cli=lambda config: config())
70
+ register("zmq", PUB=1, SNDHWM=2, Socket=object, Context=object)
71
+ register("gear_sonic.camera.composed_camera", ComposedCameraClientSensor=object)
72
+ register(
73
+ "gear_sonic.data.robot_model.instantiation.g1",
74
+ instantiate_g1_robot_model=lambda **_kwargs: None,
75
+ )
76
+ register(
77
+ "gear_sonic.scripts.run_vla_inference",
78
+ pack_latent_action_message=lambda *_args, **_kwargs: b"sonic",
79
+ )
80
+ register(
81
+ "gear_sonic.utils.data_collection.keyboard_subscriber",
82
+ DEFAULT_ZMQ_KEYBOARD_PORT=5580,
83
+ ZMQKeyboardSubscriber=object,
84
+ )
85
+ register(
86
+ "gear_sonic.utils.data_collection.transforms",
87
+ compute_projected_gravity=lambda _quat: np.array([0.0, 0.0, -1.0]),
88
+ )
89
+ register(
90
+ "gear_sonic.utils.data_collection.zmq_state_subscriber",
91
+ ZMQStateSubscriber=object,
92
+ )
93
+ register(
94
+ "gear_sonic.utils.inference.initial_poses",
95
+ LATENT_INITIAL_MOTION_TOKEN=np.zeros(64, dtype=np.float32),
96
+ )
97
+
98
+ def split_groups(robot_model, observation):
99
+ for name, indices in robot_model.groups.items():
100
+ observation["state"][name] = observation["q"][..., indices]
101
+ return observation
102
+
103
+ register(
104
+ "gear_sonic.utils.inference.vla_utils",
105
+ calculate_latency_compensated_index=lambda *_args: 0,
106
+ prepare_observation_for_eval=split_groups,
107
+ should_trigger_new_inference=lambda **_kwargs: True,
108
+ )
109
+ register(
110
+ "gear_sonic.utils.teleop.zmq.zmq_planner_sender",
111
+ build_command_message=lambda **_kwargs: b"command",
112
+ )
113
+
114
+ @classmethod
115
+ def load_client(cls):
116
+ cls.install_stubs()
117
+ name = "gear_sonic.scripts.run_vla_inference_dex1_head"
118
+ sys.modules.pop(name, None)
119
+ return importlib.import_module(name)
120
+
121
+ def test_observation_matches_checkpoint_exactly(self):
122
+ client = self.load_client()
123
+
124
+ class Latest:
125
+ def __init__(self, value):
126
+ self.value = value
127
+
128
+ def read(self):
129
+ return self.value
130
+
131
+ def get_msg(self):
132
+ return self.value
133
+
134
+ def is_fresh(self, _timeout):
135
+ return True
136
+
137
+ class RobotModel:
138
+ groups = {
139
+ "left_leg": list(range(0, 6)),
140
+ "right_leg": list(range(6, 12)),
141
+ "waist": list(range(12, 15)),
142
+ "left_arm": list(range(15, 22)),
143
+ "right_arm": list(range(22, 29)),
144
+ "left_hand": list(range(29, 36)),
145
+ "right_hand": list(range(36, 43)),
146
+ }
147
+
148
+ def get_configuration_from_actuated_joints(self, **kwargs):
149
+ return np.concatenate(
150
+ [
151
+ kwargs["body_actuated_joint_values"],
152
+ kwargs["left_hand_actuated_joint_values"],
153
+ kwargs["right_hand_actuated_joint_values"],
154
+ ]
155
+ )
156
+
157
+ camera = Latest(
158
+ {
159
+ "images": {
160
+ "ego_view": np.zeros((4, 5, 3), dtype=np.uint8),
161
+ "left_wrist": np.zeros((2, 3, 3), dtype=np.uint8),
162
+ "right_wrist": np.zeros((2, 3, 3), dtype=np.uint8),
163
+ }
164
+ }
165
+ )
166
+ body = Latest(
167
+ {
168
+ "body_q": np.zeros(29),
169
+ "base_quat": np.array([1.0, 0.0, 0.0, 0.0]),
170
+ }
171
+ )
172
+ hand = Latest({"left_hand_joints": [0.2], "right_hand_joints": [0.8]})
173
+ head = Latest({"yaw_position": 0.1, "pitch_position": -0.2})
174
+ observation = client.prepare_observation(
175
+ camera,
176
+ body,
177
+ hand,
178
+ head,
179
+ RobotModel(),
180
+ "pick up the cup",
181
+ 0.5,
182
+ )
183
+ self.assertEqual(
184
+ set(observation["video"]),
185
+ {"ego_view", "left_wrist", "right_wrist"},
186
+ )
187
+ self.assertEqual(set(observation["state"]), set(client.EXPECTED_STATE_DIMS))
188
+ self.assertEqual(observation["state"]["left_gripper"].shape, (1, 1, 1))
189
+ self.assertEqual(observation["state"]["right_gripper"].shape, (1, 1, 1))
190
+ self.assertEqual(observation["state"]["head_joints"].shape, (1, 1, 2))
191
+ self.assertNotIn("left_hand", observation["state"])
192
+ self.assertNotIn("right_hand", observation["state"])
193
+
194
+ def test_worker_calls_standard_policy_without_rtc_options(self):
195
+ client = self.load_client()
196
+ requests: queue.Queue = queue.Queue(maxsize=1)
197
+ results: queue.Queue = queue.Queue(maxsize=1)
198
+ stop = threading.Event()
199
+ busy = threading.Event()
200
+ called = []
201
+
202
+ def standard_policy(observation):
203
+ called.append(observation)
204
+ return action_chunk(), {}
205
+
206
+ worker = threading.Thread(
207
+ target=client._inference_worker_loop,
208
+ args=(
209
+ requests,
210
+ results,
211
+ stop,
212
+ busy,
213
+ lambda: {"standard": True},
214
+ standard_policy,
215
+ 40,
216
+ client.DiagnosticLogger(False),
217
+ ),
218
+ daemon=True,
219
+ )
220
+ worker.start()
221
+ requests.put(None)
222
+ chunk, _started_at = results.get(timeout=2.0)
223
+ stop.set()
224
+ worker.join(timeout=1.0)
225
+ self.assertEqual(len(called), 1)
226
+ self.assertEqual(chunk["head_joints"].shape, (40, 2))
227
+
228
+
229
+ if __name__ == "__main__":
230
+ unittest.main()
GR00T-WholeBodyControl/gear_sonic/tests/test_input_readers.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import msgpack
2
+ import msgpack_numpy as msgpack_numpy
3
+ import numpy as np
4
+
5
+ from gear_sonic.utils.teleop.input_readers import (
6
+ build_body_pose_sample,
7
+ decode_msgpack_byte_multi_array,
8
+ )
9
+
10
+
11
+ def test_decode_msgpack_byte_multi_array_from_byte_chunks():
12
+ payload = {
13
+ "timestamp": 123456789,
14
+ "joint_positions": [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]],
15
+ "joint_orientations": [[0.0, 0.0, 0.0, 1.0], [0.5, 0.5, 0.5, 0.5]],
16
+ }
17
+ packed = msgpack.packb(payload, default=msgpack_numpy.encode, use_bin_type=True)
18
+ byte_chunks = [bytes([value]) for value in packed]
19
+
20
+ decoded = decode_msgpack_byte_multi_array(
21
+ byte_chunks,
22
+ msgpack_module=msgpack,
23
+ msgpack_numpy_module=msgpack_numpy,
24
+ )
25
+
26
+ assert decoded["timestamp"] == payload["timestamp"]
27
+ assert decoded["joint_positions"] == payload["joint_positions"]
28
+ assert decoded["joint_orientations"] == payload["joint_orientations"]
29
+
30
+
31
+ def test_build_body_pose_sample_uses_existing_teleop_shape():
32
+ payload = {
33
+ "timestamp": 1_000_000_100,
34
+ "joint_positions": [[0.1, 0.2, 0.3], [0.4, 0.5, 0.6]],
35
+ "joint_orientations": [[0.0, 0.0, 0.0, 1.0], [0.0, 0.0, 1.0, 0.0]],
36
+ }
37
+
38
+ sample, stamp_ns, fps_ema = build_body_pose_sample(
39
+ payload,
40
+ prev_stamp_ns=1_000_000_000,
41
+ fps_ema=0.0,
42
+ )
43
+
44
+ assert sample is not None
45
+ assert stamp_ns == payload["timestamp"]
46
+ assert sample["body_poses_np"].shape == (24, 7)
47
+ np.testing.assert_allclose(sample["body_poses_np"][0], np.array([0.1, 0.2, 0.3, 0.0, 0.0, 0.0, 1.0]))
48
+ np.testing.assert_allclose(sample["body_poses_np"][1], np.array([0.4, 0.5, 0.6, 0.0, 0.0, 1.0, 0.0]))
49
+ assert sample["dt"] == 1e-7
50
+ assert fps_ema == 1.0 / 1e-7
GR00T-WholeBodyControl/gear_sonic/train_agent_trl.py ADDED
@@ -0,0 +1,487 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # Copyright 2025 The HuggingFace Team. All rights reserved.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ # Fix sys.path: when running as `python gear_sonic/train_agent_trl.py`, Python adds
17
+ # gear_sonic/ to sys.path[0], causing `from trl import ...` to resolve to our local
18
+ # gear_sonic/trl/ instead of the HuggingFace trl package. Replace with repo root.
19
+ import sys
20
+ import os
21
+ _script_dir = os.path.dirname(os.path.abspath(__file__))
22
+ _repo_root = os.path.dirname(_script_dir)
23
+ if _script_dir in sys.path:
24
+ sys.path.remove(_script_dir)
25
+ if _repo_root not in sys.path:
26
+ sys.path.insert(0, _repo_root)
27
+
28
+ try:
29
+ import isaaclab # noqa: F401
30
+ except ImportError:
31
+ print(
32
+ "\n"
33
+ "ERROR: Isaac Lab is required for training but not installed.\n"
34
+ "\n"
35
+ "Isaac Lab is not a pip dependency — it must be installed separately.\n"
36
+ "Follow the official guide:\n"
37
+ " https://isaac-sim.github.io/IsaacLab/main/source/setup/installation/index.html\n"
38
+ "\n"
39
+ "After installing, activate the Isaac Lab conda/venv environment\n"
40
+ "before running this script.\n"
41
+ )
42
+ sys.exit(1)
43
+
44
+ import glob
45
+ import logging
46
+ import os
47
+ from pathlib import Path
48
+ import re
49
+ import sys
50
+
51
+ from filelock import FileLock
52
+ import hydra
53
+ from hydra.core.hydra_config import HydraConfig
54
+ from hydra.utils import instantiate
55
+ from loguru import logger
56
+ from omegaconf import DictConfig, OmegaConf
57
+ import wandb
58
+ import yaml
59
+
60
+ from gear_sonic.trl.utils.common import (
61
+ custom_instantiate,
62
+ get_filtered_state_dict,
63
+ materialize_lazy_params,
64
+ wandb_run_exists,
65
+ )
66
+ from gear_sonic.utils.common import seeding
67
+ from gear_sonic.utils.config_utils import register_rl_resolvers
68
+ from gear_sonic.utils.obs_utils import get_group_term_obs_shape
69
+
70
+ register_rl_resolvers()
71
+
72
+
73
+ def resume_training(config):
74
+ if config.get("checkpoint", None) is not None:
75
+ last_existing_checkpoint = config.checkpoint
76
+ elif config.get("experiment_dir", None) is not None:
77
+ last_existing_checkpoint = os.path.join(config.experiment_dir, "last.pt")
78
+ else:
79
+ # Use experiment_dir to find the checkpoint, rather than reconstructing
80
+ # from config.project_name which can differ from the actual filesystem path.
81
+ experiment_dir_base = re.sub(r"-\d{8}_\d{6}$", "", config.experiment_dir)
82
+ checkpoints = sorted(glob.glob(os.path.join(f"{experiment_dir_base}-*", "last.pt")))
83
+ if not checkpoints:
84
+ print(f"No checkpoint found matching {experiment_dir_base}-*/last.pt, starting fresh")
85
+ return
86
+ last_existing_checkpoint = checkpoints[-1]
87
+ experiment_dir = os.path.dirname(last_existing_checkpoint)
88
+ config.experiment_dir = experiment_dir
89
+ config.checkpoint = last_existing_checkpoint
90
+ print(f"Resuming training from {last_existing_checkpoint}")
91
+
92
+
93
+ def resume_checkpoint(config):
94
+ config.checkpoint = config.checkpoint
95
+
96
+
97
+ def create_manager_env(config, device, args_cli):
98
+
99
+ # import wandb
100
+
101
+ from isaaclab.envs import (
102
+ ManagerBasedRLEnv,
103
+ )
104
+
105
+ from gear_sonic.envs.wrapper.manager_env_wrapper import ManagerEnvWrapper
106
+
107
+ env_instance_cfg = custom_instantiate(config.manager_env)
108
+
109
+ # Iteratively check the difference in attribute of env_instance_cfg1 and env_instance_cfg, print out the difference
110
+ def compare_attrs(obj1, obj2, prefix=""):
111
+ # Only compare attributes that do not start with '__' and are not methods
112
+ attrs1 = set(dir(obj1))
113
+ attrs2 = set(dir(obj2))
114
+ common_attrs = attrs1 & attrs2
115
+ for attr in sorted(common_attrs):
116
+ if (
117
+ attr.startswith("__")
118
+ or callable(getattr(obj1, attr))
119
+ or callable(getattr(obj2, attr))
120
+ ):
121
+ continue
122
+ try:
123
+ val1 = getattr(obj1, attr)
124
+ val2 = getattr(obj2, attr)
125
+ except Exception:
126
+ continue
127
+ # Recursively compare if both are objects with __dict__ or are dicts
128
+ if isinstance(val1, dict | DictConfig) and isinstance(val2, dict | DictConfig):
129
+ compare_attrs(val1, val2, prefix + attr + ".")
130
+ elif hasattr(val1, "__dict__") and hasattr(val2, "__dict__"):
131
+ compare_attrs(val1, val2, prefix + attr + ".")
132
+ else:
133
+ if isinstance(val1, list):
134
+ val1 = tuple(val1)
135
+ if isinstance(val2, list):
136
+ val2 = tuple(val2)
137
+ if val1 != val2:
138
+ print(
139
+ f"\nDifference found at '{prefix}{attr}':\n"
140
+ f" - env_instance_cfg1: {val1!r}\n"
141
+ f" - env_instance_cfg : {val2!r}\n"
142
+ )
143
+
144
+ env_instance_cfg.seed = config.seed
145
+ env_instance_cfg.sim.device = device
146
+ env_instance_cfg.config["headless"] = args_cli.headless
147
+ env = ManagerBasedRLEnv(
148
+ cfg=env_instance_cfg, render_mode="rgb_array" if not args_cli.headless else None
149
+ )
150
+
151
+ env = ManagerEnvWrapper(env, env_instance_cfg.config)
152
+ return env
153
+
154
+
155
+ @hydra.main(config_path="config", config_name="base", version_base="1.1")
156
+ def main(config: OmegaConf):
157
+ simulator_type = "IsaacSim"
158
+ env_config = config.manager_env
159
+ from transformers import HfArgumentParser
160
+ from trl import ModelConfig, PPOConfig, ScriptArguments
161
+
162
+ # Setup model components
163
+ parser = HfArgumentParser((ScriptArguments, PPOConfig, ModelConfig))
164
+
165
+ if config.get("resume", False):
166
+ resume_training(config)
167
+ elif config.get("checkpoint", None) is not None:
168
+ resume_checkpoint(config)
169
+
170
+ config.algo.trl.output_dir = str(Path(config.experiment_dir))
171
+
172
+ script_args, training_args, model_args = parser.parse_dict(config.algo.trl)
173
+
174
+ # Add exp_name from main config to training_args
175
+ training_args.exp_name = config.experiment_name
176
+
177
+ from datetime import timedelta
178
+
179
+ from accelerate import Accelerator, DistributedDataParallelKwargs, InitProcessGroupKwargs
180
+ import torch # noqa: E402
181
+
182
+ ddp_kwargs = DistributedDataParallelKwargs(find_unused_parameters=False)
183
+ kwargs = InitProcessGroupKwargs(timeout=timedelta(seconds=6000))
184
+ accelerator = Accelerator(
185
+ gradient_accumulation_steps=training_args.gradient_accumulation_steps,
186
+ kwargs_handlers=[ddp_kwargs, kwargs],
187
+ )
188
+
189
+ device = str(accelerator.device)
190
+ if device == "cuda":
191
+ device = "cuda:0"
192
+ config.multi_gpu = accelerator.num_processes > 1
193
+ if config.multi_gpu:
194
+ config.global_rank = accelerator.process_index
195
+ config.seed += accelerator.process_index
196
+ config.algo.config.global_rank = accelerator.process_index
197
+ config.algo.config.world_size = accelerator.num_processes
198
+ seeding(config.seed)
199
+
200
+ meta_path = Path(config.experiment_dir) / "meta.yaml"
201
+ if meta_path.exists():
202
+ meta = yaml.safe_load(open(meta_path))
203
+ config.wandb.wandb_id = meta["wandb_run"]
204
+ print(f"resume wandb from run: {config.wandb.wandb_id}")
205
+
206
+ unresolved_conf = OmegaConf.to_container(config, resolve=False)
207
+ if config.use_wandb and accelerator.is_main_process:
208
+ project_name = f"{config.project_name}"
209
+ run_name = config.experiment_dir.replace(f"{config.base_dir}/{project_name}/", "")
210
+ wandb_dir = Path(config.wandb.wandb_dir)
211
+ wandb_dir.mkdir(exist_ok=True, parents=True)
212
+ wandb_group = None if config.wandb.wandb_id is not None else config.wandb.wandb_group
213
+ logger.info(f"Saving wandb logs to {wandb_dir}")
214
+ wandb.init(
215
+ project=project_name,
216
+ entity=config.wandb.wandb_entity,
217
+ name=run_name,
218
+ sync_tensorboard=True,
219
+ config=unresolved_conf,
220
+ dir=wandb_dir,
221
+ id=config.wandb.wandb_id,
222
+ group=wandb_group,
223
+ resume="allow",
224
+ )
225
+
226
+ # Setup simulator similar to train_agent.py
227
+
228
+ if simulator_type == "IsaacSim":
229
+ try:
230
+ with open("./rl/simulator/isaacsim/.isaacsim_version", encoding="utf-8") as f:
231
+ DEFAULT_ISAACSIM_VERSION = f.read().strip()
232
+ except FileNotFoundError:
233
+ DEFAULT_ISAACSIM_VERSION = "4.5"
234
+
235
+ if DEFAULT_ISAACSIM_VERSION == "4.5":
236
+ from isaaclab.app import AppLauncher
237
+ elif DEFAULT_ISAACSIM_VERSION == "4.2":
238
+ logger.warning("Using IsaacSim 4.2, replacing isaaclab with omni.isaac.lab")
239
+ from omni.isaac.lab.app import AppLauncher # 4.2
240
+
241
+ # from isaaclab.app import AppLauncher # not working
242
+ # from omni.isaac.lab.app import AppLauncher
243
+
244
+ import argparse
245
+
246
+ parser = argparse.ArgumentParser(description="Train an RL agent with TRL.")
247
+ AppLauncher.add_app_launcher_args(parser)
248
+
249
+ ######################################################### ZL: fix isaacsim 4.5 rendering #########################################################
250
+ args_cli, hydra_args = parser.parse_known_args()
251
+ sys.argv = [sys.argv[0]] + hydra_args
252
+ args_cli.num_envs = config.num_envs
253
+ args_cli.seed = config.seed
254
+ args_cli.env_spacing = env_config.config.env_spacing # config.env_spacing
255
+ args_cli.output_dir = config.output_dir
256
+ # Enable cameras if enable_cameras, render_results, render_ego, or overview_camera is True
257
+ args_cli.enable_cameras = (
258
+ env_config.config.get("enable_cameras", False)
259
+ or env_config.config.get("render_results", False)
260
+ or env_config.config.get("render_ego", False)
261
+ or env_config.config.get("overview_camera", False)
262
+ )
263
+ args_cli.headless = config.headless
264
+ args_cli.multi_gpu = config.multi_gpu
265
+ args_cli.distributed = config.multi_gpu
266
+ args_cli.device = device
267
+
268
+ # Base kit args (quiet logs)
269
+ args_cli.kit_args = (
270
+ "--/log/level=error --/log/fileLogLevel=error --/log/outputStreamLevel=error"
271
+ )
272
+
273
+ # Allow air-gapped machines to use an experience file with online
274
+ # extension registries disabled, while preserving the default behavior.
275
+ offline_experience = os.environ.get("ISAACLAB_EXPERIENCE")
276
+ if offline_experience:
277
+ args_cli.experience = offline_experience
278
+
279
+ # AppLauncher can't handle multiple processes creating it at the same time so we need a lock
280
+ _lock_path = "/tmp/isaaclab_app_launcher.lock"
281
+ _local_rank = int(os.environ.get("LOCAL_RANK", 0))
282
+ with FileLock(_lock_path):
283
+ app_launcher = AppLauncher(args_cli)
284
+
285
+ simulation_app = app_launcher.app
286
+
287
+ torch.backends.cuda.matmul.allow_tf32 = True
288
+ torch.backends.cudnn.allow_tf32 = True
289
+ torch.backends.cudnn.deterministic = False
290
+ torch.backends.cudnn.benchmark = False
291
+
292
+ from gear_sonic.utils.logging import HydraLoggerBridge
293
+
294
+ # resolve=False is important otherwise overrides
295
+ # at inference time won't work properly
296
+ # also, I believe this must be done before instantiation
297
+
298
+ # logging to hydra log file
299
+ hydra_log_path = os.path.join(HydraConfig.get().runtime.output_dir, "train.log")
300
+ logger.remove()
301
+ logger.add(hydra_log_path, level="DEBUG")
302
+ console_log_level = os.environ.get("LOGURU_LEVEL", "INFO").upper()
303
+ logger.add(sys.stdout, level=console_log_level, colorize=True)
304
+ logging.basicConfig(level=logging.DEBUG)
305
+ logging.getLogger().addHandler(HydraLoggerBridge())
306
+
307
+ # Setup wandb if enabled
308
+ os.chdir(hydra.utils.get_original_cwd())
309
+
310
+ # Save config and meta BEFORE env creation so eval jobs can postprocess
311
+ # checkpoint configs even if training crashes during env init.
312
+ experiment_save_dir = Path(config.experiment_dir)
313
+ if accelerator.is_main_process:
314
+ experiment_save_dir.mkdir(exist_ok=True, parents=True)
315
+ logger.info(f"Saving config file to {experiment_save_dir}")
316
+ with open(experiment_save_dir / "config.yaml", "w") as file:
317
+ OmegaConf.save(unresolved_conf, file)
318
+ meta = {"wandb_run": wandb.run.id if wandb_run_exists() else None}
319
+ meta["max_train_steps"] = config.algo.config.num_learning_iterations
320
+ yaml.safe_dump(meta, open(meta_path, "w"))
321
+ print("saved meta:", meta)
322
+
323
+ # Initialize environment
324
+ env_config.config.save_rendering_dir = str(Path(config.experiment_dir) / "renderings_training")
325
+ env_config.config.experiment_dir = str(Path(config.experiment_dir))
326
+
327
+ env = create_manager_env(config, device, args_cli)
328
+ if config.get("replay", False):
329
+ _save_video_path = config.get("replay_save_video", None)
330
+ env.run_replay(
331
+ start_time_step=-1,
332
+ loop=config.get("replay_loop_num", True),
333
+ save_video_path=_save_video_path,
334
+ grid_spacing=config.get("replay_grid_spacing", 2.0),
335
+ )
336
+ os._exit(0)
337
+ if config.get("vplanner_replay", False):
338
+ vplanner_checkpoint = config.get("vplanner_checkpoint", None)
339
+ if vplanner_checkpoint is None:
340
+ raise ValueError("vplanner_checkpoint must be specified for vplanner_replay")
341
+ env.run_vplanner_replay(
342
+ checkpoint_path=vplanner_checkpoint,
343
+ max_frames=config.get("vplanner_max_frames", 500),
344
+ replan_interval=config.get("vplanner_replan_interval", 0),
345
+ speed=config.get("vplanner_speed", 1.0),
346
+ loop=config.get("vplanner_loop", True),
347
+ save_images=config.get("vplanner_save_images", False),
348
+ output_dir=config.get("vplanner_output_dir", None),
349
+ dof_noise=config.get("vplanner_dof_noise", 0.0),
350
+ dof_vel_noise=config.get("vplanner_dof_vel_noise", 0.0),
351
+ quat_noise=config.get("vplanner_quat_noise", 0.0),
352
+ )
353
+ os._exit(0)
354
+
355
+ ref_model = None
356
+ value_model = None
357
+ disc_model = None
358
+ # import ipdb; ipdb.set_trace()
359
+
360
+ if config.algo.config.get("use_new_actor_critic", False):
361
+ module_dim_dict = getattr(config.algo.config, "module_dim", {})
362
+ policy_backbone_kwargs = {}
363
+ critic_backbone_kwargs = {}
364
+ env.config["obs"]["obs_dims"]["actor_obs"] = env.env.observation_space["policy"].shape[-1]
365
+ env.config["obs"]["obs_dims"]["critic_obs"] = env.env.observation_space["critic"].shape[-1]
366
+ env.config["robot"]["algo_obs_dim_dict"]["actor_obs"] = env.env.observation_space[
367
+ "policy"
368
+ ].shape[-1]
369
+ env.config["robot"]["algo_obs_dim_dict"]["critic_obs"] = env.env.observation_space[
370
+ "critic"
371
+ ].shape[-1]
372
+ example_obs = env.reset(flatten_dict_obs=False)
373
+ for key in env.env.observation_space:
374
+ if key not in ["policy", "critic"]:
375
+ group_obs_dims, group_obs_names, group_obs_total_dim = get_group_term_obs_shape(
376
+ example_obs, key
377
+ )
378
+ env.config["obs"]["group_obs_dims"][key] = group_obs_dims
379
+ env.config["obs"]["group_obs_names"][key] = group_obs_names
380
+ env.config["obs"]["obs_dims"][key] = group_obs_total_dim
381
+ env.config["robot"]["algo_obs_dim_dict"][key] = group_obs_total_dim
382
+ if config.manager_env.config.get("meta_action_dim", None) is not None:
383
+ env.config["robot"]["actions_dim"] = config.manager_env.config.meta_action_dim
384
+ else:
385
+ env.config["robot"]["actions_dim"] = env.env.action_space.shape[-1]
386
+
387
+ policy = custom_instantiate(
388
+ config.algo.config.actor,
389
+ env_config=env.config,
390
+ algo_config=config.algo.config,
391
+ module_dim_dict=module_dim_dict,
392
+ backbone_kwargs=policy_backbone_kwargs,
393
+ _resolve=False,
394
+ ).to(device)
395
+
396
+ if getattr(config.algo.config, "use_dagger", False):
397
+ # Get teacher input key from config or default to "teacher"
398
+ teacher_input_key = config.algo.config.get("teacher_input_key", "teacher")
399
+ ref_model = custom_instantiate(
400
+ config.algo.config.teacher_actor,
401
+ env_config=env.config,
402
+ algo_config=config.algo.config,
403
+ module_dim_dict=module_dim_dict,
404
+ _resolve=False,
405
+ input_key=teacher_input_key,
406
+ ).to(device)
407
+ if not getattr(config.algo.config, "distill_only", False):
408
+ value_model = custom_instantiate(
409
+ config.algo.config.critic,
410
+ env_config=env.config,
411
+ algo_config=config.algo.config,
412
+ module_dim_dict=module_dim_dict,
413
+ backbone_kwargs=critic_backbone_kwargs,
414
+ _resolve=False,
415
+ ).to(device)
416
+ if config.algo.config.get("use_amp", False):
417
+ disc_model = custom_instantiate(
418
+ config.algo.config.disc,
419
+ env_config=env.config,
420
+ algo_config=config.algo.config,
421
+ module_dim_dict=module_dim_dict,
422
+ _resolve=False,
423
+ ).to(device)
424
+ else:
425
+ raise ValueError("No longer supported")
426
+
427
+ materialize_lazy_params(policy, env)
428
+
429
+ if config.algo.config.get("pretrained_model", None) is not None:
430
+ pretrained_cfg = config.algo.config.pretrained_model
431
+ sd_key = pretrained_cfg.get("state_dict_key", "state_dict")
432
+ strict = pretrained_cfg.get("strict", True)
433
+ state_dict = torch.load(pretrained_cfg.path, map_location=device, weights_only=False)[
434
+ sd_key
435
+ ]
436
+ for (
437
+ module_name,
438
+ state_dict_key,
439
+ ) in pretrained_cfg.module_mapping.items():
440
+ module = eval(module_name)
441
+ filtered_state_dict = get_filtered_state_dict(state_dict, state_dict_key)
442
+ missing, unexpected = module.load_state_dict(filtered_state_dict, strict=strict)
443
+ if missing:
444
+ logger.info(f"Pretrained loading '{module_name}': missing keys: {missing}")
445
+ if unexpected:
446
+ logger.info(f"Pretrained loading '{module_name}': unexpected keys: {unexpected}")
447
+
448
+ accelerator.wait_for_everyone()
449
+
450
+ callbacks = []
451
+ for callback in config.callbacks.values():
452
+ callbacks.append(instantiate(callback))
453
+
454
+ ################
455
+ # Training
456
+ ################
457
+ trainer = custom_instantiate(
458
+ config.trainer,
459
+ args=training_args,
460
+ config=config.algo.config,
461
+ env=env,
462
+ model=policy,
463
+ disc_model=disc_model,
464
+ value_model=value_model,
465
+ ref_model=ref_model,
466
+ use_ref_model=getattr(config.algo.config, "use_dagger", False),
467
+ train_dataset=None,
468
+ eval_dataset=None,
469
+ callbacks=callbacks,
470
+ checkpoint=config.checkpoint,
471
+ resume=config.get("resume", False),
472
+ local_seed=config.seed,
473
+ log_dir=experiment_save_dir,
474
+ accelerator=accelerator,
475
+ _resolve=False,
476
+ )
477
+
478
+ # Training loop
479
+ trainer.train()
480
+
481
+ if simulator_type == "IsaacSim":
482
+ os._exit(0)
483
+
484
+
485
+ if __name__ == "__main__":
486
+
487
+ main()