Add files using upload-large-folder tool
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- GR00T-WholeBodyControl/decoupled_wbc/__init__.py +3 -0
- GR00T-WholeBodyControl/decoupled_wbc/version.py +11 -0
- GR00T-WholeBodyControl/docs/Makefile +20 -0
- GR00T-WholeBodyControl/docs/README.md +74 -0
- GR00T-WholeBodyControl/gear_sonic.egg-info/PKG-INFO +72 -0
- GR00T-WholeBodyControl/gear_sonic.egg-info/SOURCES.txt +276 -0
- GR00T-WholeBodyControl/gear_sonic.egg-info/dependency_links.txt +1 -0
- GR00T-WholeBodyControl/gear_sonic.egg-info/requires.txt +66 -0
- GR00T-WholeBodyControl/gear_sonic.egg-info/top_level.txt +2 -0
- GR00T-WholeBodyControl/gear_sonic/__init__.py +0 -0
- GR00T-WholeBodyControl/gear_sonic/camera/__init__.py +29 -0
- GR00T-WholeBodyControl/gear_sonic/camera/composed_camera.py +692 -0
- GR00T-WholeBodyControl/gear_sonic/config/base.yaml +52 -0
- GR00T-WholeBodyControl/gear_sonic/config/base_eval.yaml +26 -0
- GR00T-WholeBodyControl/gear_sonic/config/eval_exp.yaml +34 -0
- GR00T-WholeBodyControl/gear_sonic/data/robot_model/__init__.py +5 -0
- GR00T-WholeBodyControl/gear_sonic/data/robot_model/__pycache__/__init__.cpython-310.pyc +0 -0
- GR00T-WholeBodyControl/gear_sonic/data/robot_model/__pycache__/robot_model.cpython-310.pyc +0 -0
- GR00T-WholeBodyControl/gear_sonic/data/robot_model/instantiation/__init__.py +5 -0
- GR00T-WholeBodyControl/gear_sonic/data/robot_model/instantiation/__pycache__/__init__.cpython-310.pyc +0 -0
- GR00T-WholeBodyControl/gear_sonic/data/robot_model/instantiation/__pycache__/g1.cpython-310.pyc +0 -0
- GR00T-WholeBodyControl/gear_sonic/data/robot_model/instantiation/g1.py +62 -0
- GR00T-WholeBodyControl/gear_sonic/data/robot_model/model_data/g1/g1_29dof_with_hand.urdf +1497 -0
- GR00T-WholeBodyControl/gear_sonic/data/robot_model/model_data/g1/g1_29dof_with_hand.xml +751 -0
- GR00T-WholeBodyControl/gear_sonic/data/robot_model/model_data/g1/meshes/left_wrist_pitch_link.STL +0 -0
- GR00T-WholeBodyControl/gear_sonic/data/robot_model/model_data/g1/scene_43dof.xml +30 -0
- GR00T-WholeBodyControl/gear_sonic/data/robot_model/robot_model.py +817 -0
- GR00T-WholeBodyControl/gear_sonic/data/robot_model/supplemental_info/__init__.py +7 -0
- GR00T-WholeBodyControl/gear_sonic/data/robot_model/supplemental_info/__pycache__/__init__.cpython-310.pyc +0 -0
- GR00T-WholeBodyControl/gear_sonic/data/robot_model/supplemental_info/__pycache__/robot_supplemental_info.cpython-310.pyc +0 -0
- GR00T-WholeBodyControl/gear_sonic/data/robot_model/supplemental_info/g1/__init__.py +0 -0
- GR00T-WholeBodyControl/gear_sonic/data/robot_model/supplemental_info/g1/__pycache__/__init__.cpython-310.pyc +0 -0
- GR00T-WholeBodyControl/gear_sonic/data/robot_model/supplemental_info/g1/__pycache__/g1_supplemental_info.cpython-310.pyc +0 -0
- GR00T-WholeBodyControl/gear_sonic/data/robot_model/supplemental_info/g1/g1_supplemental_info.py +334 -0
- GR00T-WholeBodyControl/gear_sonic/data/robot_model/supplemental_info/robot_supplemental_info.py +93 -0
- GR00T-WholeBodyControl/gear_sonic/eval_agent_trl.py +670 -0
- GR00T-WholeBodyControl/gear_sonic/eval_exp.py +679 -0
- GR00T-WholeBodyControl/gear_sonic/pyproject.toml +116 -0
- GR00T-WholeBodyControl/gear_sonic/scripts/launch_data_collection.py +473 -0
- GR00T-WholeBodyControl/gear_sonic/scripts/launch_inference.py +465 -0
- GR00T-WholeBodyControl/gear_sonic/scripts/pico_manager_thread_server.py +2311 -0
- GR00T-WholeBodyControl/gear_sonic/scripts/process_dataset.py +634 -0
- GR00T-WholeBodyControl/gear_sonic/scripts/run_camera_viewer.py +264 -0
- GR00T-WholeBodyControl/gear_sonic/scripts/run_data_exporter.py +963 -0
- GR00T-WholeBodyControl/gear_sonic/scripts/run_sim_loop.py +68 -0
- GR00T-WholeBodyControl/gear_sonic/scripts/run_vla_inference.py +777 -0
- GR00T-WholeBodyControl/gear_sonic/scripts/run_vla_inference_dex1_head.py +697 -0
- GR00T-WholeBodyControl/gear_sonic/tests/test_dex1_head_inference.py +230 -0
- GR00T-WholeBodyControl/gear_sonic/tests/test_input_readers.py +50 -0
- 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()
|