diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 03d9bb902..c1c468fc4 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -8,12 +8,16 @@ repos:
# Run the linter
- id: ruff
args: ["--fix"]
+ # The bundled Noitom MocapApi SDK is vendor code; keep it byte-stable.
+ exclude: ^src/plugins/noitom_mocap/third_party/mocap_api/
# Run the formatter
- id: ruff-format
+ exclude: ^src/plugins/noitom_mocap/third_party/mocap_api/
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v6.0.0
hooks:
- id: trailing-whitespace
+ exclude: ^src/plugins/noitom_mocap/third_party/mocap_api/
- id: check-symlinks
- id: destroyed-symlinks
- id: check-added-large-files
@@ -24,6 +28,7 @@ repos:
- id: check-executables-have-shebangs
- id: check-toml
- id: end-of-file-fixer
+ exclude: ^src/plugins/noitom_mocap/third_party/mocap_api/
- id: check-shebang-scripts-are-executable
- id: detect-private-key
- id: debug-statements
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 5c12c7c8e..9f8220818 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -59,6 +59,7 @@ endif()
# Options
option(BUILD_PLUGINS "Build plugins" ON)
option(BUILD_PLUGIN_OAK_CAMERA "Build OAK camera plugin (requires vcpkg for DepthAI v3.x)" OFF)
+option(BUILD_PLUGIN_NOITOM_MOCAP "Build Noitom mocap plugin (uses bundled MocapApi SDK)" OFF)
option(BUILD_EXAMPLES "Build examples" ON)
option(BUILD_EXAMPLE_TELEOP_ROS2 "Build only the teleop_ros2 example (e.g. for Docker)" OFF)
option(BUILD_TESTING "Build unit tests" ON)
@@ -187,6 +188,9 @@ if(BUILD_PLUGINS)
if(BUILD_PLUGIN_OAK_CAMERA)
add_subdirectory(src/plugins/oak)
endif()
+ if(BUILD_PLUGIN_NOITOM_MOCAP)
+ add_subdirectory(src/plugins/noitom_mocap)
+ endif()
endif()
# Formatting enforcement (runs on Linux by default)
diff --git a/LICENSES/LicenseRef-Noitom-MocapApi-SDK.txt b/LICENSES/LicenseRef-Noitom-MocapApi-SDK.txt
new file mode 100644
index 000000000..366cc8745
--- /dev/null
+++ b/LICENSES/LicenseRef-Noitom-MocapApi-SDK.txt
@@ -0,0 +1,9 @@
+Noitom MocapApi SDK License Reference
+
+The files under src/plugins/noitom_mocap/third_party/mocap_api/ are bundled
+third-party Noitom MocapApi SDK artifacts.
+
+The copied SDK drop in this repository does not include a redistributable
+license text or copyright notice. Keep those files annotated with
+LicenseRef-Noitom-MocapApi-SDK in REUSE.toml instead of assigning NVIDIA
+copyright or Apache-2.0 metadata to the vendor SDK.
diff --git a/REUSE.toml b/REUSE.toml
index 49a87ba37..bb1e5354a 100644
--- a/REUSE.toml
+++ b/REUSE.toml
@@ -41,3 +41,12 @@ path = "examples/cloudxr_mujoco_teleop/**"
precedence = "override"
SPDX-FileCopyrightText = "Copyright (c) 2026 HTC Corporation"
SPDX-License-Identifier = "Apache-2.0"
+
+# The bundled Noitom MocapApi SDK does not ship SPDX metadata in the copied
+# headers/bindings; keep the vendor path annotated instead of adding NVIDIA
+# copyright headers to third-party SDK files.
+[[annotations]]
+path = "src/plugins/noitom_mocap/third_party/mocap_api/**"
+precedence = "override"
+SPDX-FileCopyrightText = "NOASSERTION"
+SPDX-License-Identifier = "LicenseRef-Noitom-MocapApi-SDK"
diff --git a/applicationInfo.apiVersion: b/applicationInfo.apiVersion:
new file mode 100644
index 000000000..e69de29bb
diff --git a/applicationInfo.applicationName: b/applicationInfo.applicationName:
new file mode 100644
index 000000000..e69de29bb
diff --git a/applicationInfo.applicationVersion: b/applicationInfo.applicationVersion:
new file mode 100644
index 000000000..e69de29bb
diff --git a/applicationInfo.engineName: b/applicationInfo.engineName:
new file mode 100644
index 000000000..e69de29bb
diff --git a/applicationInfo.engineVersion: b/applicationInfo.engineVersion:
new file mode 100644
index 000000000..e69de29bb
diff --git a/docs/source/_static/haptikos-exoskeletons.gif b/docs/source/_static/haptikos-exoskeletons.gif
new file mode 100644
index 000000000..d838ba383
Binary files /dev/null and b/docs/source/_static/haptikos-exoskeletons.gif differ
diff --git a/docs/source/device/haptikos.rst b/docs/source/device/haptikos.rst
index f8373e7eb..39fba12d4 100644
--- a/docs/source/device/haptikos.rst
+++ b/docs/source/device/haptikos.rst
@@ -4,6 +4,19 @@
Haptikos Exoskeletons
=====================
+.. raw:: html
+
+
+
+.. image:: ../_static/haptikos-exoskeletons.gif
+ :alt: Haptikos exoskeletons used with Isaac Teleop
+ :class: align-center
+ :width: 100%
+
+.. raw:: html
+
+
+
Use the `Haptikos `_ Exoskeletons with the Isaac Teleop framework. Currently only Linux is supported. Tested on Meta Quest headsets. Other headsets with controllers may work as well.
.. contents:: On this page
diff --git a/examples/noitom/README.md b/examples/noitom/README.md
new file mode 100644
index 000000000..7ca8f8ee8
--- /dev/null
+++ b/examples/noitom/README.md
@@ -0,0 +1,203 @@
+
+
+# Noitom G1 Test Task
+
+This example registers an external Isaac Lab task that is based on
+`Isaac-PickPlace-Locomanipulation-G1-Abs-v0` and swaps its IsaacTeleop pipeline
+to read the `noitom_mocap` OpenXR tensor collection as `FullBodyPosePN` data
+defined in `src/core/schema/fbs/full_body_pn.fbs`.
+
+Noitom data is converted before it reaches this task. The plugin publishes the
+Perception Neuron full-body schema on tensor identifier `full_body_pn`; this
+example consumes it with `FullBodyTrackerPN("noitom_mocap")`.
+
+The registered task id is:
+
+```bash
+Isaac-PickPlace-Locomanipulation-G1-Noitom-Abs-v0
+```
+
+## Examples
+
+This README has two workflows:
+
+- Teleop: stream live Noitom full-body data into the G1 IsaacLab task.
+- Record and replay: capture the same `full_body_pn` stream to MCAP and play it
+ back without running the teleop task.
+
+### Example 1: Teleop
+
+
+
+Terminal 1, keep CloudXR running:
+
+```bash
+conda activate teleop
+cd ~/IsaacTeleop
+python -m isaacteleop.cloudxr
+```
+
+Terminal 2, publish Noitom data into CloudXR/OpenXR:
+
+```bash
+source ~/.cloudxr/run/cloudxr.env
+cd ~/IsaacTeleop
+./install/plugins/noitom_mocap/noitom_mocap_plugin --host= --port=
+```
+
+The default plugin collection id is `noitom_mocap`, which matches
+`NoitomG1Settings.collection_id`. If you pass `--collection-id` to the plugin,
+update `NoitomG1Settings.collection_id` to the same value.
+
+Terminal 3, run IsaacLab with the external task registration callback:
+
+```bash
+conda activate teleop
+cd ~/dependence/IsaacLab3-0
+PYTHONPATH=~/IsaacTeleop/examples/noitom:$PYTHONPATH \
+./isaaclab.sh -p scripts/environments/teleoperation/teleop_se3_agent.py \
+ --task Isaac-PickPlace-Locomanipulation-G1-Noitom-Abs-v0 \
+ --visualizer kit \
+ --xr \
+ --external_callback noitom_tasks.register_tasks
+```
+
+If CloudXR is already running in Terminal 1 and IsaacLab should not auto-launch
+another runtime, add:
+
+```bash
+--cloudxr_env none --no-auto_launch_cloudxr
+```
+
+### Example 2: Record and replay
+
+Record and replay use the same Noitom OpenXR tensor collection as the teleop
+task. Keep CloudXR and `noitom_mocap_plugin` running as in Example 1.
+
+Before recording from a development checkout, rebuild the Python package and
+make sure this shell imports the rebuilt package, not an older package installed
+in the conda environment:
+
+```bash
+cd ~/IsaacTeleop
+cmake --build build --target python_package noitom_mocap_plugin --parallel
+
+PYTHONPATH=$PWD/build/python_package/Release \
+python - <<'PY'
+import isaacteleop.deviceio as deviceio
+deviceio.FullBodyTrackerPN("noitom_mocap", 16 * 1024)
+print(deviceio.__file__)
+PY
+```
+
+Record the Noitom full-body stream:
+
+
+
+```bash
+PYTHONPATH=$PWD/build/python_package/Release \
+python examples/noitom/record_noitom_full_body.py --duration 10
+```
+
+By default the recorder reads collection `noitom_mocap`, writes MCAP channel
+base `full_body_pn`, and stores the file under `examples/noitom/recordings/`.
+Override paths or ids as needed:
+
+```bash
+python examples/noitom/record_noitom_full_body.py \
+ --collection-id noitom_mocap \
+ --channel-name full_body_pn \
+ --duration 10 \
+ --output /tmp/noitom_full_body.mcap
+```
+
+Replay the newest recording:
+
+
+
+```bash
+PYTHONPATH=$PWD/build/python_package/Release \
+python examples/noitom/replay_noitom_full_body.py
+```
+
+Replay an explicit file:
+
+```bash
+python examples/noitom/replay_noitom_full_body.py /tmp/noitom_full_body.mcap
+```
+
+`replay_noitom_full_body.py` uses the Python `mcap` package to infer playback
+duration from the file summary. If that package is unavailable in your test
+environment, pass a duration explicitly:
+
+```bash
+python examples/noitom/replay_noitom_full_body.py /tmp/noitom_full_body.mcap --duration 10
+```
+
+The resulting MCAP stores `core.FullBodyPosePNRecord` on the `full_body_pn`
+channel path. Replay it with `examples/noitom/replay_noitom_full_body.py`, not
+the PICO `FullBodyPosePicoRecord` replay path.
+
+If the recorder prints a "waiting for Noitom frames" message, check that the
+plugin is still running in the same OpenXR/CloudXR runtime and that both sides
+use the same `collection_id`.
+
+## Behavior
+
+Retargeting lives in `noitom_retargeting.py` and is wired by `noitom_tasks.py`.
+It maps Noitom `FullBodyPosePN` shoulder / elbow / wrist bones into G1 **arm joint
+deltas** for direct joint-position teleop (no Pink IK).
+
+Pipeline:
+
+```text
+FullBodyPosePN
+ → torso frame (pelvis, SPINE3, shoulders)
+ → left/right wrist SE3 deltas (shoulder, elbow, wrist)
+ → arm-length scale + two-link reach clamp
+ → left/right arm joint deltas (shoulder, elbow, wrist; 7+7)
+ → action [left_arm(7), right_arm(7), hand_joints(14), locomotion(4)]
+```
+
+Upper body uses :class:`~isaaclab.envs.mdp.actions.JointPositionAction` (not Pink IK).
+Each arm joint is retargeted from the corresponding mocap bone using the same
+torso-relative delta idea as the legacy wrist-only path.
+
+The G1 action layout is:
+
+```text
+[left_arm(7), right_arm(7), hand_joints(14), locomotion(4)]
+```
+
+Arm deltas use shoulder / elbow / wrist mocap in a torso-relative frame (position
+for shoulder–elbow–wrist chain, orientation for wrist). Hand joints and
+locomotion stay fixed for bring-up unless extended later.
+Finger retargeting is intentionally omitted.
+
+Calibration:
+
+1. After teleop **reset** (`execution_events.reset`), retargeting clears its
+ neutral reference.
+2. Hold a stable upper-body pose; the next valid frame becomes the neutral
+ reference (`calibrated=ready` in logs).
+3. Motion is applied relative to that neutral pose.
+
+Default settings:
+
+- `NoitomG1Settings` in `noitom_tasks.py` (collection, visualization)
+- `NoitomRetargetingSettings` in `noitom_retargeting.py` (scale, smoothing,
+ robot arm lengths, workspace clamps)
+
+When Kit visualization is enabled, the Noitom frame is shown as a cyan stick
+figure anchored to the robot pelvis (orange crosses mark retargeted wrist
+targets). The visualizer uses the same Y-up to Z-up conversion as the retargeter
+(`noitom_position_to_isaac`).
+
+**Calibration tip:** At reset, hold a pose close to the G1 default upper-body
+posture (elbows bent, hands near chest height). Calibration maps your neutral
+mocap pose to the robot nominal wrist targets; a large mismatch at calibration
+will show the cyan skeleton far from the orange wrist targets even when you are
+not moving.
diff --git a/examples/noitom/assets/record.gif b/examples/noitom/assets/record.gif
new file mode 100644
index 000000000..016b47a00
--- /dev/null
+++ b/examples/noitom/assets/record.gif
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:388972ec86663d67b232aff3f192fd928752612f5b68d3cc85110dacdea3a921
+size 1775108
diff --git a/examples/noitom/assets/replay.gif b/examples/noitom/assets/replay.gif
new file mode 100644
index 000000000..055089594
--- /dev/null
+++ b/examples/noitom/assets/replay.gif
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:55417b73d0cb585137ddfa2047b1e4478f8e0e0451b5ccff62d607fa41f0a026
+size 806637
diff --git a/examples/noitom/assets/teleop.gif b/examples/noitom/assets/teleop.gif
new file mode 100644
index 000000000..2bc89a05d
--- /dev/null
+++ b/examples/noitom/assets/teleop.gif
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:a9ad5aa77d996f38b4b5952e04f9df432f67456523f3bb3e7e2343daccf8dd88
+size 2678115
diff --git a/examples/noitom/noitom_reference_draw.py b/examples/noitom/noitom_reference_draw.py
new file mode 100644
index 000000000..c90493da2
--- /dev/null
+++ b/examples/noitom/noitom_reference_draw.py
@@ -0,0 +1,445 @@
+# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+"""Shared Noitom mocap skeleton placement (cyan debug draw + wrist retargeting)."""
+
+from __future__ import annotations
+
+from dataclasses import dataclass
+from typing import Any
+
+import numpy as np
+from scipy.spatial.transform import Rotation
+
+from isaacteleop.schema import BodyJointPN
+
+from noitom_retargeting import noitom_position_to_isaac
+
+# G1_29DOF_CFG spawn rot (0,0,0.7071,0.7071): pelvis +X aligns with world +Y (table forward).
+G1_ROBOT_FORWARD_XY = np.array([0.0, 1.0], dtype=np.float64)
+
+
+@dataclass(frozen=True)
+class ReferenceSkeletonLengths:
+ """G1 link lengths for posture-based reference skeleton (meters)."""
+
+ upper_arm: float = 0.28
+ forearm: float = 0.26
+ torso_segment: float = 0.07
+ neck: float = 0.08
+ head: float = 0.12
+ hand_extension: float = 0.05
+ left_shoulder_offset: tuple[float, float, float] = (0.05, 0.19, 0.30)
+ right_shoulder_offset: tuple[float, float, float] = (0.05, -0.19, 0.30)
+
+ @classmethod
+ def from_retargeting_settings(cls, settings: Any) -> ReferenceSkeletonLengths:
+ return cls(
+ upper_arm=float(settings.robot_upper_arm_length),
+ forearm=float(settings.robot_forearm_length),
+ left_shoulder_offset=tuple(
+ float(v) for v in settings.robot_left_shoulder_offset
+ ),
+ right_shoulder_offset=tuple(
+ float(v) for v in settings.robot_right_shoulder_offset
+ ),
+ )
+
+
+ARM_CHAIN_JOINTS = frozenset(
+ {
+ int(BodyJointPN.LEFT_COLLAR),
+ int(BodyJointPN.LEFT_SHOULDER),
+ int(BodyJointPN.LEFT_ELBOW),
+ int(BodyJointPN.LEFT_WRIST),
+ int(BodyJointPN.LEFT_HAND),
+ int(BodyJointPN.RIGHT_COLLAR),
+ int(BodyJointPN.RIGHT_SHOULDER),
+ int(BodyJointPN.RIGHT_ELBOW),
+ int(BodyJointPN.RIGHT_WRIST),
+ int(BodyJointPN.RIGHT_HAND),
+ }
+)
+
+
+def extract_raw_yup_positions(
+ frame: Any,
+) -> tuple[dict[int, np.ndarray], np.ndarray | None]:
+ """Read valid Noitom joint positions (Y-up meters) from a FullBodyPosePN frame."""
+ if frame.joints is None:
+ return {}, None
+ positions: dict[int, np.ndarray] = {}
+ for index in range(int(BodyJointPN.NUM_JOINTS)):
+ joint = frame.joints.joints(index)
+ if not joint.is_valid:
+ continue
+ point = joint.pose.position
+ positions[int(index)] = np.array([point.x, point.y, point.z], dtype=np.float64)
+ return positions, positions.get(int(BodyJointPN.PELVIS))
+
+
+def reference_joint_scale(
+ joint_index: int,
+ draw_scale: float,
+ calib_view: Any | None,
+ arm_chain_joints: frozenset[int] = ARM_CHAIN_JOINTS,
+) -> float:
+ if calib_view is None:
+ return draw_scale
+ if joint_index in arm_chain_joints:
+ return draw_scale * float(calib_view.arm_length_scale)
+ return draw_scale * float(calib_view.body_height_scale)
+
+
+def build_reference_skeleton_positions(
+ raw_positions: dict[int, np.ndarray],
+ pelvis_raw: np.ndarray,
+ pelvis_anchor: np.ndarray,
+ draw_scale: float,
+ calib_view: Any | None,
+ arm_chain_joints: frozenset[int] = ARM_CHAIN_JOINTS,
+) -> dict[int, np.ndarray]:
+ """Place Noitom joints at the robot pelvis anchor (Isaac Z-up, pelvis-relative)."""
+ pelvis_isaac = noitom_position_to_isaac(pelvis_raw)
+ anchor = np.asarray(pelvis_anchor, dtype=np.float64)
+ positions: dict[int, np.ndarray] = {}
+ for index, point_raw in raw_positions.items():
+ joint_scale = reference_joint_scale(
+ int(index), draw_scale, calib_view, arm_chain_joints
+ )
+ rel = (noitom_position_to_isaac(point_raw) - pelvis_isaac) * joint_scale
+ positions[int(index)] = anchor + rel
+ return positions
+
+
+def reference_torso_forward_xy(
+ positions: dict[int, np.ndarray],
+ *,
+ pelvis_index: int = int(BodyJointPN.PELVIS),
+ spine3_index: int = int(BodyJointPN.SPINE3),
+ left_shoulder_index: int = int(BodyJointPN.LEFT_SHOULDER),
+ right_shoulder_index: int = int(BodyJointPN.RIGHT_SHOULDER),
+) -> np.ndarray | None:
+ pelvis = positions.get(pelvis_index)
+ spine3 = positions.get(spine3_index)
+ left_shoulder = positions.get(left_shoulder_index)
+ right_shoulder = positions.get(right_shoulder_index)
+ if (
+ pelvis is None
+ or spine3 is None
+ or left_shoulder is None
+ or right_shoulder is None
+ ):
+ return None
+ up = spine3 - pelvis
+ right = right_shoulder - left_shoulder
+ if float(np.linalg.norm(up)) < 1e-5 or float(np.linalg.norm(right)) < 1e-5:
+ return None
+ up = up / np.linalg.norm(up)
+ right = right / np.linalg.norm(right)
+ forward = np.cross(up, right)
+ forward_xy = forward[:2]
+ norm = float(np.linalg.norm(forward_xy))
+ if norm < 1e-5:
+ return None
+ return forward_xy / norm
+
+
+def signed_yaw_xy(from_xy: np.ndarray, to_xy: np.ndarray) -> float:
+ source = np.asarray(from_xy, dtype=np.float64)[:2]
+ target = np.asarray(to_xy, dtype=np.float64)[:2]
+ source_norm = float(np.linalg.norm(source))
+ target_norm = float(np.linalg.norm(target))
+ if source_norm < 1e-8 or target_norm < 1e-8:
+ return 0.0
+ source = source / source_norm
+ target = target / target_norm
+ cross = source[0] * target[1] - source[1] * target[0]
+ dot = float(np.clip(np.dot(source, target), -1.0, 1.0))
+ return float(np.arctan2(cross, dot))
+
+
+def rotate_reference_about_anchor(
+ positions: dict[int, np.ndarray],
+ anchor: np.ndarray,
+ yaw: float,
+) -> dict[int, np.ndarray]:
+ if abs(yaw) < 1e-6:
+ return positions
+ rot = Rotation.from_euler("z", yaw)
+ anchor_vec = np.asarray(anchor, dtype=np.float64)
+ return {
+ index: anchor_vec + rot.apply(pos - anchor_vec)
+ for index, pos in positions.items()
+ }
+
+
+def _unit_direction(
+ vector: np.ndarray,
+ fallback: np.ndarray | None = None,
+) -> np.ndarray:
+ vec = np.asarray(vector, dtype=np.float64)
+ norm = float(np.linalg.norm(vec))
+ if norm < 1e-6:
+ if fallback is not None:
+ return _unit_direction(fallback)
+ return np.array([1.0, 0.0, 0.0], dtype=np.float64)
+ return vec / norm
+
+
+def _pelvis_frame_offset_to_world(offset: np.ndarray) -> np.ndarray:
+ """Map G1 pelvis-frame offset to world when the robot faces scene +Y."""
+ ox, oy, oz = np.asarray(offset, dtype=np.float64)
+ return np.array([-oy, ox, oz], dtype=np.float64)
+
+
+def apply_robot_link_lengths(
+ positions: dict[int, np.ndarray],
+ pelvis_anchor: np.ndarray,
+ lengths: ReferenceSkeletonLengths,
+ *,
+ length_scale: float = 1.0,
+ arm_length_scale: float = 1.0,
+ shoulder_span_scale: float = 1.0,
+) -> dict[int, np.ndarray]:
+ """Rebuild upper body in order: head match -> shoulder width -> arm segment lengths."""
+ anchor = np.asarray(pelvis_anchor, dtype=np.float64)
+ scale = float(length_scale)
+ arm_scale = float(arm_length_scale)
+ span_scale = float(shoulder_span_scale)
+ out = dict(positions)
+ out[int(BodyJointPN.PELVIS)] = anchor.copy()
+
+ torso_nominal_total = (
+ 3.0 * lengths.torso_segment + lengths.neck + lengths.head
+ ) * scale
+ torso_scale = 1.0
+ head_src = positions.get(int(BodyJointPN.HEAD))
+ pelvis_src = positions.get(int(BodyJointPN.PELVIS))
+ if head_src is not None and pelvis_src is not None and torso_nominal_total > 1e-6:
+ src_head_dist = float(np.linalg.norm(head_src - pelvis_src))
+ torso_scale = float(np.clip(src_head_dist / torso_nominal_total, 0.6, 1.8))
+
+ torso_chain = (
+ (int(BodyJointPN.SPINE1), lengths.torso_segment),
+ (int(BodyJointPN.SPINE2), lengths.torso_segment),
+ (int(BodyJointPN.SPINE3), lengths.torso_segment),
+ (int(BodyJointPN.NECK), lengths.neck),
+ (int(BodyJointPN.HEAD), lengths.head),
+ )
+ parent_robot = anchor
+ parent_mocap = positions.get(int(BodyJointPN.PELVIS), anchor)
+ up_fallback = np.array([0.0, 0.0, 1.0], dtype=np.float64)
+ for joint_index, segment_length in torso_chain:
+ child_mocap = positions.get(joint_index)
+ if child_mocap is None:
+ continue
+ direction = _unit_direction(child_mocap - parent_mocap, fallback=up_fallback)
+ robot_pos = parent_robot + direction * (segment_length * scale * torso_scale)
+ out[joint_index] = robot_pos
+ parent_robot = robot_pos
+ parent_mocap = child_mocap
+
+ left_shoulder_src = positions.get(int(BodyJointPN.LEFT_SHOULDER))
+ right_shoulder_src = positions.get(int(BodyJointPN.RIGHT_SHOULDER))
+ spine3_robot = out.get(int(BodyJointPN.SPINE3))
+ spine3_src = positions.get(int(BodyJointPN.SPINE3))
+
+ default_left = anchor + _pelvis_frame_offset_to_world(
+ np.asarray(lengths.left_shoulder_offset, dtype=np.float64)
+ )
+ default_right = anchor + _pelvis_frame_offset_to_world(
+ np.asarray(lengths.right_shoulder_offset, dtype=np.float64)
+ )
+ default_span = float(np.linalg.norm(default_left - default_right))
+ mocap_span_scale = 1.0
+ shoulder_axis = np.array([0.0, 1.0, 0.0], dtype=np.float64)
+ shoulder_center = 0.5 * (default_left + default_right)
+ if left_shoulder_src is not None and right_shoulder_src is not None:
+ src_vec = left_shoulder_src - right_shoulder_src
+ src_span = float(np.linalg.norm(src_vec))
+ if src_span > 1e-6:
+ shoulder_axis = src_vec / src_span
+ if default_span > 1e-6:
+ mocap_span_scale = float(np.clip(src_span / default_span, 0.7, 1.15))
+ src_center = 0.5 * (left_shoulder_src + right_shoulder_src)
+ if spine3_robot is not None and spine3_src is not None:
+ shoulder_center = spine3_robot + (src_center - spine3_src)
+ else:
+ shoulder_center = src_center.copy()
+ # Follow mocap shoulder height more aggressively; keep only a loose guard band
+ # so shoulders can actually track instead of being locked near nominal.
+ nominal_shoulder_center_z = float(0.5 * (default_left[2] + default_right[2]))
+ blended_center_z = (
+ 0.85 * float(shoulder_center[2]) + 0.15 * nominal_shoulder_center_z
+ )
+ shoulder_center_z = float(
+ np.clip(
+ blended_center_z,
+ nominal_shoulder_center_z - 0.12,
+ nominal_shoulder_center_z + 0.10,
+ )
+ )
+ shoulder_center = shoulder_center.copy()
+ shoulder_center[2] = shoulder_center_z
+
+ shoulder_half_span = 0.5 * default_span * mocap_span_scale * scale * span_scale
+ left_shoulder_robot = shoulder_center + shoulder_axis * shoulder_half_span
+ right_shoulder_robot = shoulder_center - shoulder_axis * shoulder_half_span
+
+ arm_specs = (
+ (
+ left_shoulder_robot,
+ int(BodyJointPN.LEFT_SHOULDER),
+ int(BodyJointPN.LEFT_ELBOW),
+ int(BodyJointPN.LEFT_WRIST),
+ int(BodyJointPN.LEFT_HAND),
+ ),
+ (
+ right_shoulder_robot,
+ int(BodyJointPN.RIGHT_SHOULDER),
+ int(BodyJointPN.RIGHT_ELBOW),
+ int(BodyJointPN.RIGHT_WRIST),
+ int(BodyJointPN.RIGHT_HAND),
+ ),
+ )
+ for shoulder, shoulder_index, elbow_index, wrist_index, hand_index in arm_specs:
+ elbow_mocap = positions.get(elbow_index)
+ wrist_mocap = positions.get(wrist_index)
+ shoulder_mocap = positions.get(shoulder_index, shoulder)
+ if elbow_mocap is None or wrist_mocap is None:
+ out[shoulder_index] = shoulder
+ continue
+ upper_dir = _unit_direction(
+ elbow_mocap - shoulder_mocap,
+ fallback=np.array([0.0, 0.0, -1.0], dtype=np.float64),
+ )
+ elbow = shoulder + upper_dir * (lengths.upper_arm * scale * arm_scale)
+ fore_dir = _unit_direction(wrist_mocap - elbow_mocap, fallback=upper_dir)
+ wrist = elbow + fore_dir * (lengths.forearm * scale * arm_scale)
+ out[shoulder_index] = shoulder
+ out[elbow_index] = elbow
+ out[wrist_index] = wrist
+ out[hand_index] = wrist + fore_dir * (
+ lengths.hand_extension * scale * arm_scale
+ )
+
+ spine3 = out.get(int(BodyJointPN.SPINE3))
+ if spine3 is not None:
+ for collar_index, shoulder_index in (
+ (int(BodyJointPN.LEFT_COLLAR), int(BodyJointPN.LEFT_SHOULDER)),
+ (int(BodyJointPN.RIGHT_COLLAR), int(BodyJointPN.RIGHT_SHOULDER)),
+ ):
+ shoulder_pos = out.get(shoulder_index)
+ if shoulder_pos is not None:
+ out[collar_index] = 0.5 * (spine3 + shoulder_pos)
+
+ return out
+
+
+def align_reference_skeleton_to_robot(
+ positions: dict[int, np.ndarray],
+ pelvis_anchor: np.ndarray,
+ *,
+ pelvis_index: int = int(BodyJointPN.PELVIS),
+ spine3_index: int = int(BodyJointPN.SPINE3),
+ left_shoulder_index: int = int(BodyJointPN.LEFT_SHOULDER),
+ right_shoulder_index: int = int(BodyJointPN.RIGHT_SHOULDER),
+ robot_forward_xy: np.ndarray | None = None,
+) -> dict[int, np.ndarray]:
+ """Rotate the skeleton about pelvis so it faces the G1 robot (+Y in scene)."""
+ forward_xy = reference_torso_forward_xy(
+ positions,
+ pelvis_index=pelvis_index,
+ spine3_index=spine3_index,
+ left_shoulder_index=left_shoulder_index,
+ right_shoulder_index=right_shoulder_index,
+ )
+ if forward_xy is None:
+ return positions
+ target_xy = G1_ROBOT_FORWARD_XY if robot_forward_xy is None else robot_forward_xy
+ yaw = signed_yaw_xy(forward_xy, target_xy)
+ return rotate_reference_about_anchor(positions, pelvis_anchor, yaw)
+
+
+def place_aligned_reference_skeleton(
+ raw_positions: dict[int, np.ndarray],
+ pelvis_raw: np.ndarray,
+ pelvis_anchor: np.ndarray,
+ draw_scale: float = 1.0,
+ calib_view: Any | None = None,
+ *,
+ use_robot_link_lengths: bool = True,
+ link_lengths: ReferenceSkeletonLengths | None = None,
+ length_scale: float = 1.0,
+ arm_length_scale: float = 1.0,
+ shoulder_span_scale: float = 1.0,
+) -> dict[int, np.ndarray]:
+ """Build pelvis-anchored skeleton and align facing with the G1 locomanipulation robot."""
+ direction_scale = 1.0 if use_robot_link_lengths else draw_scale
+ direction_calib = None if use_robot_link_lengths else calib_view
+ positions = build_reference_skeleton_positions(
+ raw_positions,
+ pelvis_raw,
+ pelvis_anchor,
+ direction_scale,
+ direction_calib,
+ )
+ positions = align_reference_skeleton_to_robot(positions, pelvis_anchor)
+ if use_robot_link_lengths:
+ lengths = link_lengths or ReferenceSkeletonLengths()
+ positions = apply_robot_link_lengths(
+ positions,
+ pelvis_anchor,
+ lengths,
+ length_scale=length_scale * draw_scale,
+ arm_length_scale=arm_length_scale,
+ shoulder_span_scale=shoulder_span_scale,
+ )
+ return positions
+
+
+def aligned_reference_skeleton_from_frame(
+ frame: Any,
+ pelvis_anchor: np.ndarray,
+ draw_scale: float = 1.0,
+ calib_view: Any | None = None,
+ *,
+ use_robot_link_lengths: bool = True,
+ link_lengths: ReferenceSkeletonLengths | None = None,
+ length_scale: float = 1.0,
+ arm_length_scale: float = 1.0,
+ shoulder_span_scale: float = 1.0,
+) -> dict[int, np.ndarray]:
+ """Full pipeline: FullBodyPosePN -> robot-aligned joint positions in Isaac world."""
+ raw_positions, pelvis_raw = extract_raw_yup_positions(frame)
+ if pelvis_raw is None or not raw_positions:
+ return {}
+ return place_aligned_reference_skeleton(
+ raw_positions,
+ pelvis_raw,
+ pelvis_anchor,
+ draw_scale,
+ calib_view,
+ use_robot_link_lengths=use_robot_link_lengths,
+ link_lengths=link_lengths,
+ length_scale=length_scale,
+ arm_length_scale=arm_length_scale,
+ shoulder_span_scale=shoulder_span_scale,
+ )
+
+
+__all__ = [
+ "ARM_CHAIN_JOINTS",
+ "G1_ROBOT_FORWARD_XY",
+ "ReferenceSkeletonLengths",
+ "align_reference_skeleton_to_robot",
+ "aligned_reference_skeleton_from_frame",
+ "apply_robot_link_lengths",
+ "build_reference_skeleton_positions",
+ "extract_raw_yup_positions",
+ "place_aligned_reference_skeleton",
+ "reference_torso_forward_xy",
+ "rotate_reference_about_anchor",
+ "signed_yaw_xy",
+]
diff --git a/examples/noitom/noitom_retargeting.py b/examples/noitom/noitom_retargeting.py
new file mode 100644
index 000000000..0885630dc
--- /dev/null
+++ b/examples/noitom/noitom_retargeting.py
@@ -0,0 +1,1851 @@
+# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+"""Noitom full-body to G1 wrist SE3 retargeting for locomanipulation teleop.
+
+Uses **posture-based** arm retargeting: mocap bone *directions* with G1 link
+lengths (not scaled human joint positions). Wrist SE(3) targets feed Pink IK.
+"""
+
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+from typing import Any
+
+import numpy as np
+from scipy.spatial.transform import Rotation, Slerp
+
+from isaacteleop.retargeting_engine.interface import (
+ BaseRetargeter,
+ ParameterState,
+ RetargeterIOType,
+)
+from isaacteleop.retargeting_engine.interface.retargeter_core_types import (
+ ComputeContext,
+ RetargeterIO,
+)
+from isaacteleop.retargeting_engine.interface.tunable_parameter import FloatParameter
+from isaacteleop.retargeting_engine.interface.tensor_group_type import TensorGroupType
+from isaacteleop.retargeting_engine.tensor_types import DLDataType, NDArrayType
+from isaacteleop.retargeting_engine.deviceio_source_nodes import (
+ DeviceIOFullBodyPosePNTracked,
+)
+from isaacteleop.schema import BodyJointPN
+
+# PNS/Noitom Y-up -> Isaac Z-up. PNS forward is -Z; axis remap maps it to Isaac -Y.
+# Retarget / debug draw then apply operator_faces_robot (+180 deg Z) to align with G1 (+Y).
+_NOITOM_TO_ISAAC = np.array(
+ [[-1.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0, 1.0, 0.0]],
+ dtype=np.float64,
+)
+_COORD_ROT = Rotation.from_matrix(_NOITOM_TO_ISAAC)
+
+_DEFAULT_LEFT_WRIST_POS = np.array([-0.18, 0.1, 0.8], dtype=np.float64)
+_DEFAULT_RIGHT_WRIST_POS = np.array([0.18, 0.1, 0.8], dtype=np.float64)
+# G1 locomanipulation VR teleop wrist orientations (see locomanipulation_g1_env_cfg).
+_DEFAULT_LEFT_WRIST_QUAT = np.array([-0.2706, 0.6533, 0.2706, 0.6533], dtype=np.float64)
+_DEFAULT_RIGHT_WRIST_QUAT = np.array([-0.7071, 0.0, 0.7071, 0.0], dtype=np.float64)
+_DEFAULT_ROBOT_PELVIS = np.array([0.0, 0.0, 0.72], dtype=np.float64)
+# Approximate G1 shoulder origins in pelvis frame (Isaac Z-up, +Y left).
+_ROBOT_LEFT_SHOULDER_OFFSET = np.array([0.05, 0.19, 0.30], dtype=np.float64)
+_ROBOT_RIGHT_SHOULDER_OFFSET = np.array([0.05, -0.19, 0.30], dtype=np.float64)
+# Torso segment lengths for posture-based reference skeleton (meters, pelvis frame).
+_ROBOT_TORSO_SEGMENT_Z = 0.07
+_ROBOT_NECK_SEGMENT = 0.08
+_ROBOT_HEAD_SEGMENT = 0.12
+_ROBOT_HAND_EXTENSION = 0.05
+_DELTA_LIMIT = np.array([0.65, 0.65, 0.65], dtype=np.float64)
+
+
+@dataclass
+class NoitomRetargetingSettings:
+ """Tunable retargeting parameters for Noitom-driven G1 wrists."""
+
+ # Fraction of mocap pose applied relative to calibrated neutral (not link length).
+ motion_scale: float = 0.55
+ # Reduce motion when the solved arm chain is nearly straight (elbow singularity).
+ arm_extension_soft_limit: float = 0.72
+ # Minimum interior elbow angle (rad) when reconstructing the forearm direction.
+ min_elbow_interior_angle: float = 0.65
+ # Scale motion when the operator arm span exceeds the G1 link lengths.
+ human_reach_margin: float = 0.96
+ # Cap how much operator torso twist rotates arm targets (radians).
+ max_torso_yaw_delta: float = 0.22
+ # Fraction of torso yaw applied to arms (lower = arms ignore body twist).
+ torso_yaw_arm_influence: float = 0.35
+ position_smoothing: float = 0.85
+ rotation_smoothing: float = 0.75
+ robot_upper_arm_length: float = 0.28
+ robot_forearm_length: float = 0.26
+ arm_scale_min: float = 0.5
+ arm_scale_max: float = 1.5
+ robot_pelvis_world: np.ndarray = field(
+ default_factory=lambda: _DEFAULT_ROBOT_PELVIS.copy()
+ )
+ robot_left_shoulder_offset: np.ndarray = field(
+ default_factory=lambda: _ROBOT_LEFT_SHOULDER_OFFSET.copy()
+ )
+ robot_right_shoulder_offset: np.ndarray = field(
+ default_factory=lambda: _ROBOT_RIGHT_SHOULDER_OFFSET.copy()
+ )
+ nominal_left_wrist_pos: np.ndarray = field(
+ default_factory=lambda: _DEFAULT_LEFT_WRIST_POS.copy()
+ )
+ nominal_right_wrist_pos: np.ndarray = field(
+ default_factory=lambda: _DEFAULT_RIGHT_WRIST_POS.copy()
+ )
+ nominal_left_wrist_quat_xyzw: np.ndarray = field(
+ default_factory=lambda: _DEFAULT_LEFT_WRIST_QUAT.copy()
+ )
+ nominal_right_wrist_quat_xyzw: np.ndarray = field(
+ default_factory=lambda: _DEFAULT_RIGHT_WRIST_QUAT.copy()
+ )
+ # Mocap wrist orientation does not match G1 wrist_yaw_link; lock by default.
+ track_wrist_orientation: bool = False
+ # Operator stands facing the robot (mirror L/R in horizontal plane).
+ operator_faces_robot: bool = True
+ # Drive Pink IK wrists to the cyan skeleton wrist joints (shared placement frame).
+ track_aligned_mocap_wrists: bool = True
+ # Rebuild cyan skeleton with G1 link lengths (bone directions, not human joint spacing).
+ reference_use_robot_link_lengths: bool = True
+ # Global multiplier on G1 segment lengths for the shared reference skeleton.
+ reference_length_scale: float = 1.0
+ # Additional scale applied only to upper-arm/forearm segments in reference skeleton.
+ reference_arm_length_scale: float = 0.7
+ # Scale left-right shoulder span in the reference skeleton (lower = narrower shoulders).
+ reference_shoulder_span_scale: float = 0.82
+ # Retain mocap pose via bone directions + robot link lengths (not joint positions).
+ use_posture_based_arms: bool = True
+ # Blend G1 nominal wrist quat with forearm-aligned frame (helps Pink IK converge).
+ wrist_orientation_forearm_blend: float = 0.35
+ # Feed posture-based elbow positions into Pink IK (narrows shoulder/elbow null space).
+ track_elbow_ik_targets: bool = True
+ # Feed posture-based shoulder positions into Pink IK (locks shoulder placement).
+ track_shoulder_ik_targets: bool = True
+ delta_limit: np.ndarray = field(default_factory=lambda: _DELTA_LIMIT.copy())
+ sync_nominal_at_calibration: bool = True
+
+
+@dataclass
+class SE3Pose:
+ """Pose in Isaac Z-up world frame (position meters, quaternion xyzw)."""
+
+ position: np.ndarray
+ quaternion_xyzw: np.ndarray
+
+ def as_action_pose(self) -> np.ndarray:
+ return np.concatenate(
+ [
+ self.position.astype(np.float32),
+ self.quaternion_xyzw.astype(np.float32),
+ ]
+ )
+
+ @staticmethod
+ def from_nominal(position: np.ndarray, quaternion_xyzw: np.ndarray) -> SE3Pose:
+ return SE3Pose(position.copy(), quaternion_xyzw.copy())
+
+
+@dataclass
+class ArmIkTargets:
+ """Pink IK frame targets for one update (wrist, elbow, shoulder per arm)."""
+
+ left_wrist: SE3Pose
+ right_wrist: SE3Pose
+ left_elbow: SE3Pose
+ right_elbow: SE3Pose
+ left_shoulder: SE3Pose
+ right_shoulder: SE3Pose
+
+
+@dataclass
+class NoitomCalibrationView:
+ """Read-only calibration snapshot for debug visualization alignment."""
+
+ pelvis_world: np.ndarray
+ body_yaw_isaac: float
+ arm_length_scale: float
+ body_height_scale: float
+
+
+@dataclass
+class _TorsoFrame:
+ origin: np.ndarray
+ rotation: Rotation
+
+
+@dataclass
+class _ArmCalibration:
+ shoulder_torso: np.ndarray
+ shoulder_world: np.ndarray
+ elbow_world: np.ndarray
+ wrist_pos_torso: np.ndarray
+ wrist_rot_torso: Rotation
+ wrist_rel_pelvis: np.ndarray
+ wrist_world: np.ndarray
+ upper_arm_length: float
+ forearm_length: float
+
+
+@dataclass
+class _CalibrationState:
+ torso: _TorsoFrame
+ left: _ArmCalibration
+ right: _ArmCalibration
+ arm_length_scale: float
+ body_height_scale: float
+ body_yaw_isaac: float
+ pelvis_world: np.ndarray
+ nominal_left: SE3Pose
+ nominal_right: SE3Pose
+ nominal_left_elbow: SE3Pose
+ nominal_right_elbow: SE3Pose
+ nominal_left_shoulder: SE3Pose
+ nominal_right_shoulder: SE3Pose
+
+
+class NoitomG1Retargeter(BaseRetargeter):
+ """Retarget Noitom upper-body motion to G1 arm SE3 targets for Pink IK."""
+
+ def __init__(
+ self,
+ settings: NoitomRetargetingSettings | None = None,
+ name: str = "noitom_g1_retargeter",
+ ) -> None:
+ self._settings = settings or NoitomRetargetingSettings()
+ self._nominal_left = SE3Pose.from_nominal(
+ self._settings.nominal_left_wrist_pos,
+ self._settings.nominal_left_wrist_quat_xyzw,
+ )
+ self._nominal_right = SE3Pose.from_nominal(
+ self._settings.nominal_right_wrist_pos,
+ self._settings.nominal_right_wrist_quat_xyzw,
+ )
+ self._calibration: _CalibrationState | None = None
+ self._latest_torso: _TorsoFrame | None = None
+ self._smoothed_left = SE3Pose.from_nominal(
+ self._nominal_left.position, self._nominal_left.quaternion_xyzw
+ )
+ self._smoothed_right = SE3Pose.from_nominal(
+ self._nominal_right.position, self._nominal_right.quaternion_xyzw
+ )
+ self._smoothed_left_elbow = self._default_elbow_pose(is_left=True)
+ self._smoothed_right_elbow = self._default_elbow_pose(is_left=False)
+ self._smoothed_left_shoulder = self._default_shoulder_pose(is_left=True)
+ self._smoothed_right_shoulder = self._default_shoulder_pose(is_left=False)
+
+ param_state = ParameterState(
+ name,
+ parameters=[
+ FloatParameter(
+ "motion_scale",
+ "Pose amplitude vs calibrated neutral (0=hold, 1=full mocap posture).",
+ default_value=self._settings.motion_scale,
+ min_value=0.0,
+ max_value=1.0,
+ step_size=0.05,
+ sync_fn=lambda v: setattr(self._settings, "motion_scale", v),
+ ),
+ FloatParameter(
+ "position_smoothing",
+ "Position smoothing alpha (0=hold last, 1=instant).",
+ default_value=self._settings.position_smoothing,
+ min_value=0.0,
+ max_value=1.0,
+ step_size=0.05,
+ sync_fn=lambda v: setattr(self._settings, "position_smoothing", v),
+ ),
+ FloatParameter(
+ "rotation_smoothing",
+ "Rotation smoothing alpha (0=hold last, 1=instant).",
+ default_value=self._settings.rotation_smoothing,
+ min_value=0.0,
+ max_value=1.0,
+ step_size=0.05,
+ sync_fn=lambda v: setattr(self._settings, "rotation_smoothing", v),
+ ),
+ FloatParameter(
+ "robot_upper_arm_length",
+ "Robot upper arm length for reach clamping [m].",
+ default_value=self._settings.robot_upper_arm_length,
+ min_value=0.05,
+ max_value=0.5,
+ step_size=0.01,
+ sync_fn=lambda v: setattr(
+ self._settings, "robot_upper_arm_length", v
+ ),
+ ),
+ FloatParameter(
+ "robot_forearm_length",
+ "Robot forearm length for reach clamping [m].",
+ default_value=self._settings.robot_forearm_length,
+ min_value=0.05,
+ max_value=0.5,
+ step_size=0.01,
+ sync_fn=lambda v: setattr(
+ self._settings, "robot_forearm_length", v
+ ),
+ ),
+ FloatParameter(
+ "arm_scale_min",
+ "Minimum arm length scale (robot/human ratio floor).",
+ default_value=self._settings.arm_scale_min,
+ min_value=0.1,
+ max_value=1.0,
+ step_size=0.05,
+ sync_fn=lambda v: setattr(self._settings, "arm_scale_min", v),
+ ),
+ FloatParameter(
+ "arm_scale_max",
+ "Maximum arm length scale (robot/human ratio ceiling).",
+ default_value=self._settings.arm_scale_max,
+ min_value=1.0,
+ max_value=3.0,
+ step_size=0.05,
+ sync_fn=lambda v: setattr(self._settings, "arm_scale_max", v),
+ ),
+ ],
+ )
+
+ super().__init__(name=name, parameter_state=param_state)
+
+ @property
+ def is_calibrated(self) -> bool:
+ return self._calibration is not None
+
+ @property
+ def awaiting_calibration(self) -> bool:
+ return self._calibration is None
+
+ @property
+ def current_left(self) -> SE3Pose:
+ return self._smoothed_left
+
+ @property
+ def current_right(self) -> SE3Pose:
+ return self._smoothed_right
+
+ @property
+ def current_left_elbow(self) -> SE3Pose:
+ return self._smoothed_left_elbow
+
+ @property
+ def current_right_elbow(self) -> SE3Pose:
+ return self._smoothed_right_elbow
+
+ @property
+ def current_left_shoulder(self) -> SE3Pose:
+ return self._smoothed_left_shoulder
+
+ @property
+ def current_right_shoulder(self) -> SE3Pose:
+ return self._smoothed_right_shoulder
+
+ @property
+ def current_arm_targets(self) -> ArmIkTargets:
+ return ArmIkTargets(
+ left_wrist=self._smoothed_left,
+ right_wrist=self._smoothed_right,
+ left_elbow=self._smoothed_left_elbow,
+ right_elbow=self._smoothed_right_elbow,
+ left_shoulder=self._smoothed_left_shoulder,
+ right_shoulder=self._smoothed_right_shoulder,
+ )
+
+ @property
+ def calibration_view(self) -> NoitomCalibrationView | None:
+ if self._calibration is None:
+ return None
+ calib = self._calibration
+ return NoitomCalibrationView(
+ pelvis_world=calib.pelvis_world.copy(),
+ body_yaw_isaac=calib.body_yaw_isaac,
+ arm_length_scale=calib.arm_length_scale,
+ body_height_scale=calib.body_height_scale,
+ )
+
+ @property
+ def body_yaw_isaac(self) -> float:
+ if self._latest_torso is None:
+ return 0.0
+ return _compute_torso_yaw(self._latest_torso)
+
+ @property
+ def body_yaw_delta(self) -> float:
+ if self._calibration is None:
+ return 0.0
+ return self.body_yaw_isaac - self._calibration.body_yaw_isaac
+
+ @property
+ def retargeting_settings(self) -> NoitomRetargetingSettings:
+ return self._settings
+
+ @property
+ def neutral_arms(self) -> tuple[_ArmCalibration, _ArmCalibration] | None:
+ if self._calibration is None:
+ return None
+ return self._calibration.left, self._calibration.right
+
+ def clear_calibration(self) -> None:
+ self._calibration = None
+ self._latest_torso = None
+ self._smoothed_left = SE3Pose.from_nominal(
+ self._nominal_left.position, self._nominal_left.quaternion_xyzw
+ )
+ self._smoothed_right = SE3Pose.from_nominal(
+ self._nominal_right.position, self._nominal_right.quaternion_xyzw
+ )
+ self._smoothed_left_elbow = self._default_elbow_pose(is_left=True)
+ self._smoothed_right_elbow = self._default_elbow_pose(is_left=False)
+ self._smoothed_left_shoulder = self._default_shoulder_pose(is_left=True)
+ self._smoothed_right_shoulder = self._default_shoulder_pose(is_left=False)
+
+ def _default_shoulder_pose(self, is_left: bool) -> SE3Pose:
+ shoulder = _shoulder_world_robot(self._settings, 0.0, is_left)
+ upper_dir = np.array([0.0, 0.0, -1.0], dtype=np.float64)
+ nominal_quat = (
+ self._settings.nominal_left_wrist_quat_xyzw
+ if is_left
+ else self._settings.nominal_right_wrist_quat_xyzw
+ )
+ quat = _elbow_quat_for_ik(upper_dir, nominal_quat, self._settings)
+ return SE3Pose(shoulder, quat)
+
+ def _default_elbow_pose(self, is_left: bool) -> SE3Pose:
+ shoulder = _shoulder_world_robot(self._settings, 0.0, is_left)
+ upper_dir = np.array([0.0, 0.0, -1.0], dtype=np.float64)
+ elbow = shoulder + upper_dir * self._settings.robot_upper_arm_length
+ nominal_quat = (
+ self._settings.nominal_left_wrist_quat_xyzw
+ if is_left
+ else self._settings.nominal_right_wrist_quat_xyzw
+ )
+ quat = _elbow_quat_for_ik(upper_dir, nominal_quat, self._settings)
+ return SE3Pose(elbow, quat)
+
+ def calibrate(self, frame: Any) -> bool:
+ self._sync_parameters_from_state()
+ parsed = _parse_upper_body(frame)
+ if parsed is None:
+ return False
+ torso, left, right, pelvis_world = parsed
+ self._latest_torso = torso
+ human_arm = (
+ left.upper_arm_length
+ + left.forearm_length
+ + right.upper_arm_length
+ + right.forearm_length
+ ) * 0.5
+ robot_arm = (
+ self._settings.robot_upper_arm_length + self._settings.robot_forearm_length
+ )
+ if human_arm < 1e-4:
+ return False
+ raw_scale = robot_arm / human_arm
+ arm_scale = float(
+ np.clip(
+ raw_scale, self._settings.arm_scale_min, self._settings.arm_scale_max
+ )
+ )
+ shoulder_world = torso.origin + torso.rotation.apply(left.shoulder_torso)
+ shoulder_rel_z = float((shoulder_world - pelvis_world)[2])
+ robot_shoulder_z = float(self._settings.robot_left_shoulder_offset[2])
+ if shoulder_rel_z > 0.1:
+ raw_body_scale = robot_shoulder_z / shoulder_rel_z
+ else:
+ raw_body_scale = arm_scale
+ body_height_scale = float(np.clip(raw_body_scale, 0.45, 1.15))
+
+ if self._settings.sync_nominal_at_calibration:
+ if self._settings.track_aligned_mocap_wrists:
+ nominal_left, nominal_right = _nominal_wrists_from_aligned_frame(
+ frame,
+ self._settings,
+ arm_scale,
+ body_height_scale,
+ pelvis_world,
+ )
+ if nominal_left is None or nominal_right is None:
+ return False
+ elif self._settings.use_posture_based_arms:
+ nominal_left = _wrist_pose_from_posture(
+ left,
+ self._settings,
+ is_left=True,
+ yaw_delta=0.0,
+ nominal_quat=self._settings.nominal_left_wrist_quat_xyzw,
+ )
+ nominal_right = _wrist_pose_from_posture(
+ right,
+ self._settings,
+ is_left=False,
+ yaw_delta=0.0,
+ nominal_quat=self._settings.nominal_right_wrist_quat_xyzw,
+ )
+ else:
+ nominal_left = SE3Pose.from_nominal(
+ self._nominal_left.position, self._nominal_left.quaternion_xyzw
+ )
+ nominal_right = SE3Pose.from_nominal(
+ self._nominal_right.position, self._nominal_right.quaternion_xyzw
+ )
+ else:
+ nominal_left = SE3Pose.from_nominal(
+ self._nominal_left.position, self._nominal_left.quaternion_xyzw
+ )
+ nominal_right = SE3Pose.from_nominal(
+ self._nominal_right.position, self._nominal_right.quaternion_xyzw
+ )
+
+ nominal_left_elbow = self._default_elbow_pose(is_left=True)
+ nominal_right_elbow = self._default_elbow_pose(is_left=False)
+ if (
+ self._settings.track_elbow_ik_targets
+ and self._settings.sync_nominal_at_calibration
+ ):
+ elbow_pair = _nominal_elbows_from_aligned_frame(
+ frame,
+ self._settings,
+ arm_scale,
+ body_height_scale,
+ pelvis_world,
+ )
+ if elbow_pair is not None:
+ nominal_left_elbow, nominal_right_elbow = elbow_pair
+
+ nominal_left_shoulder = self._default_shoulder_pose(is_left=True)
+ nominal_right_shoulder = self._default_shoulder_pose(is_left=False)
+ if (
+ self._settings.track_shoulder_ik_targets
+ and self._settings.sync_nominal_at_calibration
+ ):
+ shoulder_pair = _nominal_shoulders_from_aligned_frame(
+ frame,
+ self._settings,
+ arm_scale,
+ body_height_scale,
+ pelvis_world,
+ )
+ if shoulder_pair is not None:
+ nominal_left_shoulder, nominal_right_shoulder = shoulder_pair
+
+ self._calibration = _CalibrationState(
+ torso=torso,
+ left=left,
+ right=right,
+ arm_length_scale=arm_scale,
+ body_height_scale=body_height_scale,
+ body_yaw_isaac=_compute_torso_yaw(torso),
+ pelvis_world=pelvis_world,
+ nominal_left=nominal_left,
+ nominal_right=nominal_right,
+ nominal_left_elbow=nominal_left_elbow,
+ nominal_right_elbow=nominal_right_elbow,
+ nominal_left_shoulder=nominal_left_shoulder,
+ nominal_right_shoulder=nominal_right_shoulder,
+ )
+ self._smoothed_left = nominal_left
+ self._smoothed_right = nominal_right
+ self._smoothed_left_elbow = nominal_left_elbow
+ self._smoothed_right_elbow = nominal_right_elbow
+ self._smoothed_left_shoulder = nominal_left_shoulder
+ self._smoothed_right_shoulder = nominal_right_shoulder
+ return True
+
+ def retarget(self, frame: Any) -> ArmIkTargets | None:
+ self._sync_parameters_from_state()
+ if self._calibration is None:
+ return None
+ parsed = _parse_upper_body(frame)
+ if parsed is None:
+ return None
+
+ torso, left, right, pelvis_world = parsed
+ self._latest_torso = torso
+ calib = self._calibration
+ if self._settings.track_aligned_mocap_wrists:
+ left_target = _wrist_target_from_aligned_skeleton(
+ frame,
+ calib,
+ self._settings,
+ is_left=True,
+ )
+ right_target = _wrist_target_from_aligned_skeleton(
+ frame,
+ calib,
+ self._settings,
+ is_left=False,
+ )
+ if left_target is None or right_target is None:
+ return None
+ else:
+ left_target = _solve_wrist_target(
+ torso=torso,
+ arm=left,
+ pelvis_world=pelvis_world,
+ neutral=calib.left,
+ neutral_torso=calib.torso,
+ nominal=calib.nominal_left,
+ calib_yaw=calib.body_yaw_isaac,
+ arm_length_scale=calib.arm_length_scale,
+ settings=self._settings,
+ is_left=True,
+ )
+ right_target = _solve_wrist_target(
+ torso=torso,
+ arm=right,
+ pelvis_world=pelvis_world,
+ neutral=calib.right,
+ neutral_torso=calib.torso,
+ nominal=calib.nominal_right,
+ calib_yaw=calib.body_yaw_isaac,
+ arm_length_scale=calib.arm_length_scale,
+ settings=self._settings,
+ is_left=False,
+ )
+ self._smoothed_left = _smooth_pose(
+ self._smoothed_left,
+ left_target,
+ self._settings.position_smoothing,
+ self._settings.rotation_smoothing,
+ )
+ self._smoothed_right = _smooth_pose(
+ self._smoothed_right,
+ right_target,
+ self._settings.position_smoothing,
+ self._settings.rotation_smoothing,
+ )
+ left_elbow_target = self._smoothed_left_elbow
+ right_elbow_target = self._smoothed_right_elbow
+ if self._settings.track_elbow_ik_targets:
+ if self._settings.track_aligned_mocap_wrists:
+ left_elbow_target = _elbow_target_from_aligned_skeleton(
+ frame, calib, self._settings, is_left=True
+ )
+ right_elbow_target = _elbow_target_from_aligned_skeleton(
+ frame, calib, self._settings, is_left=False
+ )
+ else:
+ yaw_delta = _resolve_yaw_delta(
+ _compute_torso_yaw(torso) - calib.body_yaw_isaac, self._settings
+ )
+ left_elbow_target = _solve_elbow_target(
+ arm=left,
+ neutral=calib.left,
+ nominal=calib.nominal_left_elbow,
+ settings=self._settings,
+ yaw_delta=yaw_delta,
+ is_left=True,
+ )
+ right_elbow_target = _solve_elbow_target(
+ arm=right,
+ neutral=calib.right,
+ nominal=calib.nominal_right_elbow,
+ settings=self._settings,
+ yaw_delta=yaw_delta,
+ is_left=False,
+ )
+ if left_elbow_target is None or right_elbow_target is None:
+ return None
+ self._smoothed_left_elbow = _smooth_pose(
+ self._smoothed_left_elbow,
+ left_elbow_target,
+ self._settings.position_smoothing,
+ self._settings.rotation_smoothing,
+ )
+ self._smoothed_right_elbow = _smooth_pose(
+ self._smoothed_right_elbow,
+ right_elbow_target,
+ self._settings.position_smoothing,
+ self._settings.rotation_smoothing,
+ )
+ left_shoulder_target = self._smoothed_left_shoulder
+ right_shoulder_target = self._smoothed_right_shoulder
+ if self._settings.track_shoulder_ik_targets:
+ if self._settings.track_aligned_mocap_wrists:
+ left_shoulder_target = _shoulder_target_from_aligned_skeleton(
+ frame, calib, self._settings, is_left=True
+ )
+ right_shoulder_target = _shoulder_target_from_aligned_skeleton(
+ frame, calib, self._settings, is_left=False
+ )
+ else:
+ yaw_delta = _resolve_yaw_delta(
+ _compute_torso_yaw(torso) - calib.body_yaw_isaac, self._settings
+ )
+ left_shoulder_target = _solve_shoulder_target(
+ arm=left,
+ neutral=calib.left,
+ nominal=calib.nominal_left_shoulder,
+ settings=self._settings,
+ yaw_delta=yaw_delta,
+ is_left=True,
+ )
+ right_shoulder_target = _solve_shoulder_target(
+ arm=right,
+ neutral=calib.right,
+ nominal=calib.nominal_right_shoulder,
+ settings=self._settings,
+ yaw_delta=yaw_delta,
+ is_left=False,
+ )
+ if left_shoulder_target is None or right_shoulder_target is None:
+ return None
+ self._smoothed_left_shoulder = _smooth_pose(
+ self._smoothed_left_shoulder,
+ left_shoulder_target,
+ self._settings.position_smoothing,
+ self._settings.rotation_smoothing,
+ )
+ self._smoothed_right_shoulder = _smooth_pose(
+ self._smoothed_right_shoulder,
+ right_shoulder_target,
+ self._settings.position_smoothing,
+ self._settings.rotation_smoothing,
+ )
+ return self.current_arm_targets
+
+ def input_spec(self) -> RetargeterIOType:
+ return {"full_body_tracked": DeviceIOFullBodyPosePNTracked()}
+
+ def output_spec(self) -> RetargeterIOType:
+ wrist_type = NDArrayType(
+ "pose", shape=(7,), dtype=DLDataType.FLOAT, dtype_bits=32
+ )
+ return {
+ "left_wrist": TensorGroupType("left_wrist", [wrist_type]),
+ "right_wrist": TensorGroupType("right_wrist", [wrist_type]),
+ "body_yaw_delta": TensorGroupType(
+ "body_yaw_delta",
+ [NDArrayType("yaw", shape=(1,), dtype=DLDataType.FLOAT, dtype_bits=32)],
+ ),
+ }
+
+ def _compute_fn(
+ self,
+ inputs: RetargeterIO,
+ outputs: RetargeterIO,
+ context: ComputeContext,
+ ) -> None:
+ if context.execution_events.reset:
+ self.clear_calibration()
+
+ tracked = inputs["full_body_tracked"][0]
+ frame = tracked.data
+
+ if frame is not None:
+ if self._calibration is None:
+ self.calibrate(frame)
+ else:
+ self.retarget(frame)
+
+ outputs["left_wrist"][0] = self._smoothed_left.as_action_pose()
+ outputs["right_wrist"][0] = self._smoothed_right.as_action_pose()
+ outputs["body_yaw_delta"][0] = np.float32(self.body_yaw_delta)
+
+
+def noitom_position_to_isaac(position: np.ndarray) -> np.ndarray:
+ """Convert a Noitom Y-up position vector to Isaac Z-up."""
+ return (_NOITOM_TO_ISAAC @ np.asarray(position, dtype=np.float64)).astype(
+ np.float64
+ )
+
+
+def noitom_quaternion_to_isaac(quaternion_xyzw: np.ndarray) -> np.ndarray:
+ """Convert a Noitom Y-up quaternion (xyzw) to Isaac Z-up."""
+ rot = Rotation.from_quat(_normalize_quat(quaternion_xyzw))
+ return _normalize_quat((_COORD_ROT * rot * _COORD_ROT.inv()).as_quat())
+
+
+def map_point_to_robot_frame(
+ noitom_point_yup: np.ndarray,
+ pelvis_yup: np.ndarray,
+ calib: NoitomCalibrationView | None,
+ current_yaw: float,
+ robot_pelvis: np.ndarray,
+ draw_scale: float = 1.0,
+ *,
+ operator_faces_robot: bool = True,
+ length_scale: float | None = None,
+) -> np.ndarray:
+ """Map a Noitom joint position into the robot simulation frame for debug draw."""
+ point = noitom_position_to_isaac(noitom_point_yup)
+ pelvis = noitom_position_to_isaac(pelvis_yup)
+ rel = point - pelvis
+ if calib is None:
+ offset = rel * draw_scale
+ if operator_faces_robot:
+ offset = Rotation.from_euler("z", np.pi).apply(offset)
+ return robot_pelvis + offset
+ yaw_delta = current_yaw - calib.body_yaw_isaac
+ scale = calib.arm_length_scale if length_scale is None else length_scale
+ offset = _map_mocap_rel_to_robot_offset(
+ rel,
+ scale,
+ draw_scale,
+ yaw_delta,
+ operator_faces_robot,
+ )
+ return robot_pelvis + offset
+
+
+def _normalize_quat(quat_xyzw: np.ndarray) -> np.ndarray:
+ quat = np.asarray(quat_xyzw, dtype=np.float64)
+ norm = np.linalg.norm(quat)
+ if norm < 1e-8:
+ return np.array([0.0, 0.0, 0.0, 1.0], dtype=np.float64)
+ return quat / norm
+
+
+def _point_to_array(point: Any) -> np.ndarray:
+ return np.array([point.x, point.y, point.z], dtype=np.float64)
+
+
+def _quat_to_array(point: Any) -> np.ndarray:
+ return _normalize_quat(
+ np.array([point.x, point.y, point.z, point.w], dtype=np.float64)
+ )
+
+
+def _joint_pose(frame: Any, joint_index: BodyJointPN | int) -> SE3Pose | None:
+ if frame.joints is None:
+ return None
+ joint = frame.joints.joints(int(joint_index))
+ if not joint.is_valid:
+ return None
+ pos = _point_to_array(joint.pose.position)
+ quat = _quat_to_array(joint.pose.orientation)
+ if not np.all(np.isfinite(pos)) or not np.all(np.isfinite(quat)):
+ return None
+ return SE3Pose(
+ noitom_position_to_isaac(pos),
+ noitom_quaternion_to_isaac(quat),
+ )
+
+
+def _build_torso_frame(frame: Any) -> _TorsoFrame | None:
+ pelvis = _joint_pose(frame, BodyJointPN.PELVIS)
+ spine = _joint_pose(frame, BodyJointPN.SPINE3)
+ left_shoulder = _joint_pose(frame, BodyJointPN.LEFT_SHOULDER)
+ right_shoulder = _joint_pose(frame, BodyJointPN.RIGHT_SHOULDER)
+ if (
+ pelvis is None
+ or spine is None
+ or left_shoulder is None
+ or right_shoulder is None
+ ):
+ return None
+
+ up = spine.position - pelvis.position
+ right = right_shoulder.position - left_shoulder.position
+ if np.linalg.norm(up) < 1e-5 or np.linalg.norm(right) < 1e-5:
+ return None
+ up /= np.linalg.norm(up)
+ right /= np.linalg.norm(right)
+ forward = np.cross(up, right)
+ if np.linalg.norm(forward) < 1e-5:
+ return None
+ forward /= np.linalg.norm(forward)
+ right = np.cross(forward, up)
+ right /= np.linalg.norm(right)
+ rotation = Rotation.from_matrix(np.column_stack([right, forward, up]))
+ return _TorsoFrame(origin=spine.position.copy(), rotation=rotation)
+
+
+def _compute_torso_yaw(torso: _TorsoFrame) -> float:
+ forward = torso.rotation.as_matrix()[:, 1]
+ return float(np.arctan2(forward[1], forward[0]))
+
+
+def _pose_to_torso(pose: SE3Pose, torso: _TorsoFrame) -> tuple[np.ndarray, Rotation]:
+ pos_torso = torso.rotation.inv().apply(pose.position - torso.origin)
+ rot_torso = torso.rotation.inv() * Rotation.from_quat(pose.quaternion_xyzw)
+ return pos_torso, rot_torso
+
+
+def _parse_arm(
+ frame: Any,
+ torso: _TorsoFrame,
+ pelvis_world: np.ndarray,
+ shoulder_index: BodyJointPN,
+ elbow_index: BodyJointPN,
+ wrist_index: BodyJointPN,
+) -> _ArmCalibration | None:
+ shoulder = _joint_pose(frame, shoulder_index)
+ elbow = _joint_pose(frame, elbow_index)
+ wrist = _joint_pose(frame, wrist_index)
+ if shoulder is None or elbow is None or wrist is None:
+ return None
+
+ upper_arm_length = float(np.linalg.norm(elbow.position - shoulder.position))
+ forearm_length = float(np.linalg.norm(wrist.position - elbow.position))
+ if upper_arm_length < 1e-4 or forearm_length < 1e-4:
+ return None
+
+ wrist_pos_torso, wrist_rot_torso = _pose_to_torso(wrist, torso)
+ shoulder_torso = torso.rotation.inv().apply(shoulder.position - torso.origin)
+ wrist_rel_pelvis = wrist.position - pelvis_world
+ return _ArmCalibration(
+ shoulder_torso=shoulder_torso,
+ shoulder_world=shoulder.position.copy(),
+ elbow_world=elbow.position.copy(),
+ wrist_pos_torso=wrist_pos_torso,
+ wrist_rot_torso=wrist_rot_torso,
+ wrist_rel_pelvis=wrist_rel_pelvis,
+ wrist_world=wrist.position.copy(),
+ upper_arm_length=upper_arm_length,
+ forearm_length=forearm_length,
+ )
+
+
+def _parse_upper_body(
+ frame: Any,
+) -> tuple[_TorsoFrame, _ArmCalibration, _ArmCalibration, np.ndarray] | None:
+ pelvis_pose = _joint_pose(frame, BodyJointPN.PELVIS)
+ torso = _build_torso_frame(frame)
+ if pelvis_pose is None or torso is None:
+ return None
+ pelvis_world = pelvis_pose.position.copy()
+ left = _parse_arm(
+ frame,
+ torso,
+ pelvis_world,
+ BodyJointPN.LEFT_SHOULDER,
+ BodyJointPN.LEFT_ELBOW,
+ BodyJointPN.LEFT_WRIST,
+ )
+ right = _parse_arm(
+ frame,
+ torso,
+ pelvis_world,
+ BodyJointPN.RIGHT_SHOULDER,
+ BodyJointPN.RIGHT_ELBOW,
+ BodyJointPN.RIGHT_WRIST,
+ )
+ if left is None or right is None:
+ return None
+ return torso, left, right, pelvis_world
+
+
+def _resolve_yaw_delta(yaw_delta: float, settings: NoitomRetargetingSettings) -> float:
+ """Limit torso twist fed into arm FK (prevents waist+arm IK deadlock)."""
+ influenced = yaw_delta * settings.torso_yaw_arm_influence
+ limit = settings.max_torso_yaw_delta
+ return float(np.clip(influenced, -limit, limit))
+
+
+def _map_mocap_direction_to_robot(
+ direction_isaac: np.ndarray,
+ yaw_delta: float,
+ operator_faces_robot: bool,
+) -> np.ndarray:
+ """Rotate a unit bone direction from mocap into the robot frame (no length scale)."""
+ direction = np.asarray(direction_isaac, dtype=np.float64)
+ norm = float(np.linalg.norm(direction))
+ if norm < 1e-6:
+ return np.array([1.0, 0.0, 0.0], dtype=np.float64)
+ direction = direction / norm
+ if operator_faces_robot:
+ direction = Rotation.from_euler("z", np.pi).apply(direction)
+ return Rotation.from_euler("z", yaw_delta).apply(direction)
+
+
+def _shoulder_world_robot(
+ settings: NoitomRetargetingSettings,
+ yaw_delta: float,
+ is_left: bool,
+) -> np.ndarray:
+ anchor = settings.robot_pelvis_world.astype(np.float64)
+ return anchor + _shoulder_offset_robot(settings, yaw_delta, is_left)
+
+
+def _arm_fk_robot(
+ arm: _ArmCalibration,
+ settings: NoitomRetargetingSettings,
+ yaw_delta: float,
+ is_left: bool,
+) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
+ """FK along mocap bone directions using G1 upper-arm and forearm lengths."""
+ shoulder_robot = _shoulder_world_robot(settings, yaw_delta, is_left)
+ upper_dir = _map_mocap_direction_to_robot(
+ arm.elbow_world - arm.shoulder_world,
+ yaw_delta,
+ settings.operator_faces_robot,
+ )
+ forearm_dir = _map_mocap_direction_to_robot(
+ arm.wrist_world - arm.elbow_world,
+ yaw_delta,
+ settings.operator_faces_robot,
+ )
+ upper_len = settings.robot_upper_arm_length
+ forearm_len = settings.robot_forearm_length
+ elbow_robot = shoulder_robot + upper_dir * upper_len
+ wrist_robot = elbow_robot + forearm_dir * forearm_len
+ return shoulder_robot, elbow_robot, wrist_robot
+
+
+def _slerp_unit_direction(
+ neutral_dir: np.ndarray, full_dir: np.ndarray, blend: float
+) -> np.ndarray:
+ """Interpolate unit bone directions (keeps FK chain valid after FK)."""
+ t = float(np.clip(blend, 0.0, 1.0))
+ mixed = (1.0 - t) * neutral_dir + t * full_dir
+ norm = float(np.linalg.norm(mixed))
+ if norm < 1e-6:
+ return full_dir
+ return mixed / norm
+
+
+def _arm_direction_dot(upper_dir: np.ndarray, forearm_dir: np.ndarray) -> float:
+ return float(np.dot(upper_dir, forearm_dir))
+
+
+def _unit_direction(
+ vector: np.ndarray, fallback: np.ndarray | None = None
+) -> np.ndarray:
+ vec = np.asarray(vector, dtype=np.float64)
+ norm = float(np.linalg.norm(vec))
+ if norm < 1e-6:
+ if fallback is not None:
+ return _unit_direction(fallback)
+ return np.array([1.0, 0.0, 0.0], dtype=np.float64)
+ return vec / norm
+
+
+def _elbow_interior_angle(upper_dir: np.ndarray, forearm_dir: np.ndarray) -> float:
+ """Angle (rad) between upper-arm and forearm unit directions; 0 = fully extended."""
+ dot = float(
+ np.clip(
+ _arm_direction_dot(
+ _unit_direction(upper_dir), _unit_direction(forearm_dir)
+ ),
+ -1.0,
+ 1.0,
+ )
+ )
+ return float(np.arccos(dot))
+
+
+def _forearm_dir_from_elbow_angle(
+ upper_dir: np.ndarray,
+ forearm_hint: np.ndarray,
+ elbow_angle: float,
+) -> np.ndarray:
+ """Rebuild a forearm unit vector with the given interior elbow angle."""
+ upper = _unit_direction(upper_dir)
+ hint = _unit_direction(forearm_hint, fallback=upper)
+ perp = hint - upper * float(np.dot(hint, upper))
+ perp_norm = float(np.linalg.norm(perp))
+ if perp_norm < 1e-6:
+ perp = np.cross(upper, np.array([0.0, 0.0, 1.0], dtype=np.float64))
+ perp_norm = float(np.linalg.norm(perp))
+ if perp_norm < 1e-6:
+ perp = np.cross(upper, np.array([0.0, 1.0, 0.0], dtype=np.float64))
+ perp_norm = float(np.linalg.norm(perp))
+ perp = perp / max(perp_norm, 1e-8)
+ angle = float(np.clip(elbow_angle, 0.0, np.pi - 1e-3))
+ forearm = np.cos(angle) * upper + np.sin(angle) * perp
+ return _unit_direction(forearm, fallback=upper)
+
+
+def _human_robot_reach_scale(
+ arm: _ArmCalibration, settings: NoitomRetargetingSettings
+) -> float:
+ """Shrink motion when the operator arm is longer than the fixed G1 chain."""
+ human_reach = arm.upper_arm_length + arm.forearm_length
+ robot_reach = settings.robot_upper_arm_length + settings.robot_forearm_length
+ if human_reach <= robot_reach + 1e-4:
+ return 1.0
+ return float(
+ np.clip(robot_reach * settings.human_reach_margin / human_reach, 0.25, 1.0)
+ )
+
+
+def _effective_motion_scale(
+ base_scale: float, extension_dot: float, soft_limit: float
+) -> float:
+ """Shrink motion when the target arm chain is near full extension."""
+ scale = float(np.clip(base_scale, 0.0, 1.0))
+ if extension_dot <= soft_limit:
+ return scale
+ penalty = (extension_dot - soft_limit) / max(1e-3, 1.0 - soft_limit)
+ return scale * (1.0 - 0.7 * float(np.clip(penalty, 0.0, 1.0)))
+
+
+def _bend_forearm_direction(
+ upper_dir: np.ndarray, forearm_dir: np.ndarray, target_dot: float = 0.55
+) -> np.ndarray:
+ """Pull forearm direction off a straight line to avoid elbow singularities."""
+ upper = upper_dir / (np.linalg.norm(upper_dir) + 1e-8)
+ forearm = forearm_dir / (np.linalg.norm(forearm_dir) + 1e-8)
+ if _arm_direction_dot(upper, forearm) <= target_dot:
+ return forearm
+ axis = np.cross(upper, np.array([0.0, 0.0, 1.0], dtype=np.float64))
+ if float(np.linalg.norm(axis)) < 1e-4:
+ axis = np.cross(upper, np.array([0.0, 1.0, 0.0], dtype=np.float64))
+ axis = axis / (np.linalg.norm(axis) + 1e-8)
+ bent = forearm.copy()
+ for _ in range(8):
+ if _arm_direction_dot(upper, bent) <= target_dot:
+ break
+ bent = Rotation.from_rotvec(axis * 0.12).apply(bent)
+ bent = bent / (np.linalg.norm(bent) + 1e-8)
+ return bent
+
+
+def _arm_chain_from_directions(
+ shoulder_robot: np.ndarray,
+ upper_dir: np.ndarray,
+ forearm_dir: np.ndarray,
+ settings: NoitomRetargetingSettings,
+) -> tuple[np.ndarray, np.ndarray]:
+ upper_len = settings.robot_upper_arm_length
+ forearm_len = settings.robot_forearm_length
+ elbow = shoulder_robot + upper_dir * upper_len
+ wrist = elbow + forearm_dir * forearm_len
+ wrist = _clamp_reach(shoulder_robot, wrist, upper_len, forearm_len)
+ fore_vec = wrist - elbow
+ fore_dist = float(np.linalg.norm(fore_vec))
+ if fore_dist > 1e-6 and fore_dist > forearm_len:
+ wrist = elbow + fore_vec * (forearm_len / fore_dist)
+ return elbow, wrist
+
+
+def _arm_fk_robot_blended(
+ arm: _ArmCalibration,
+ neutral_arm: _ArmCalibration,
+ settings: NoitomRetargetingSettings,
+ yaw_delta: float,
+ is_left: bool,
+) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
+ """Blend mocap posture (bone directions + elbow angle), then FK with G1 link lengths."""
+ shoulder_robot = _shoulder_world_robot(settings, yaw_delta, is_left)
+ upper_n = _map_mocap_direction_to_robot(
+ neutral_arm.elbow_world - neutral_arm.shoulder_world,
+ yaw_delta,
+ settings.operator_faces_robot,
+ )
+ upper_f = _map_mocap_direction_to_robot(
+ arm.elbow_world - arm.shoulder_world,
+ yaw_delta,
+ settings.operator_faces_robot,
+ )
+ fore_n = _map_mocap_direction_to_robot(
+ neutral_arm.wrist_world - neutral_arm.elbow_world,
+ yaw_delta,
+ settings.operator_faces_robot,
+ )
+ fore_f = _map_mocap_direction_to_robot(
+ arm.wrist_world - arm.elbow_world,
+ yaw_delta,
+ settings.operator_faces_robot,
+ )
+ scale = _effective_motion_scale(
+ settings.motion_scale,
+ _arm_direction_dot(upper_f, fore_f),
+ settings.arm_extension_soft_limit,
+ )
+ yaw_factor = 1.0 - 0.45 * min(
+ 1.0, abs(yaw_delta) / max(settings.max_torso_yaw_delta, 1e-3)
+ )
+ scale *= max(0.25, yaw_factor)
+ scale *= _human_robot_reach_scale(arm, settings)
+ upper_d = _slerp_unit_direction(upper_n, upper_f, scale)
+
+ theta_n = _elbow_interior_angle(upper_n, fore_n)
+ theta_f = _elbow_interior_angle(upper_f, fore_f)
+ theta_d = (1.0 - scale) * theta_n + scale * theta_f
+ min_theta = max(
+ settings.min_elbow_interior_angle,
+ float(np.arccos(np.clip(settings.arm_extension_soft_limit, -1.0, 1.0))),
+ )
+ theta_d = float(np.clip(max(theta_d, min_theta), min_theta, np.pi - 1e-3))
+ fore_d = _forearm_dir_from_elbow_angle(upper_d, fore_f, theta_d)
+
+ if _arm_direction_dot(upper_d, fore_d) > settings.arm_extension_soft_limit:
+ fore_d = _bend_forearm_direction(upper_d, fore_d)
+
+ elbow_neutral, wrist_neutral = _arm_chain_from_directions(
+ shoulder_robot, upper_n, fore_n, settings
+ )
+ elbow_full, wrist_full = _arm_chain_from_directions(
+ shoulder_robot, upper_d, fore_d, settings
+ )
+ elbow_robot = elbow_neutral + scale * (elbow_full - elbow_neutral)
+ wrist_robot = wrist_neutral + scale * (wrist_full - wrist_neutral)
+ wrist_robot = _clamp_reach(
+ shoulder_robot,
+ wrist_robot,
+ settings.robot_upper_arm_length,
+ settings.robot_forearm_length,
+ )
+ return shoulder_robot, elbow_robot, wrist_robot
+
+
+def _wrist_quat_from_forearm(
+ forearm_dir: np.ndarray,
+ nominal_quat: np.ndarray,
+ blend: float,
+) -> np.ndarray:
+ """Derive a wrist quaternion consistent with the solved forearm direction."""
+ forward = np.asarray(forearm_dir, dtype=np.float64)
+ norm = float(np.linalg.norm(forward))
+ if norm < 1e-6:
+ return _normalize_quat(nominal_quat)
+ forward = forward / norm
+ world_up = np.array([0.0, 0.0, 1.0], dtype=np.float64)
+ if abs(float(np.dot(forward, world_up))) > 0.92:
+ world_up = np.array([0.0, 1.0, 0.0], dtype=np.float64)
+ right = np.cross(forward, world_up)
+ right_norm = float(np.linalg.norm(right))
+ if right_norm < 1e-6:
+ return _normalize_quat(nominal_quat)
+ right = right / right_norm
+ up = np.cross(right, forward)
+ aligned = Rotation.from_matrix(np.column_stack([right, forward, up]))
+ blend_clamped = float(np.clip(blend, 0.0, 1.0))
+ if blend_clamped <= 0.0:
+ return _normalize_quat(nominal_quat)
+ if blend_clamped >= 1.0:
+ return _normalize_quat(aligned.as_quat())
+ nominal_rot = Rotation.from_quat(nominal_quat)
+ slerp = Slerp(
+ [0.0, 1.0],
+ Rotation.concatenate([nominal_rot, aligned]),
+ )
+ return _normalize_quat(slerp([blend_clamped]).as_quat()[0])
+
+
+def _wrist_pose_from_posture(
+ arm: _ArmCalibration,
+ settings: NoitomRetargetingSettings,
+ is_left: bool,
+ yaw_delta: float,
+ nominal_quat: np.ndarray,
+) -> SE3Pose:
+ _shoulder_robot, _elbow_robot, wrist_robot = _arm_fk_robot(
+ arm, settings, yaw_delta, is_left
+ )
+ forearm_dir = _map_mocap_direction_to_robot(
+ arm.wrist_world - arm.elbow_world,
+ yaw_delta,
+ settings.operator_faces_robot,
+ )
+ quat = _wrist_quat_for_ik(
+ forearm_dir, nominal_quat, settings, track_orientation=False
+ )
+ return SE3Pose(wrist_robot, quat)
+
+
+def _wrist_quat_for_ik(
+ forearm_dir: np.ndarray,
+ nominal_quat: np.ndarray,
+ settings: NoitomRetargetingSettings,
+ track_orientation: bool,
+) -> np.ndarray:
+ if track_orientation:
+ return _normalize_quat(nominal_quat)
+ return _wrist_quat_from_forearm(
+ forearm_dir,
+ nominal_quat,
+ settings.wrist_orientation_forearm_blend,
+ )
+
+
+def compute_robot_reference_positions(
+ frame: Any,
+ settings: NoitomRetargetingSettings,
+ calib: NoitomCalibrationView,
+ current_yaw: float,
+ neutral_left: _ArmCalibration,
+ neutral_right: _ArmCalibration,
+) -> dict[int, np.ndarray]:
+ """Build a robot-proportioned reference skeleton (posture, not scaled joint dots)."""
+ parsed = _parse_upper_body(frame)
+ if parsed is None:
+ return {}
+ torso, left, right, _pelvis_world = parsed
+ yaw_delta = _resolve_yaw_delta(current_yaw - calib.body_yaw_isaac, settings)
+ anchor = settings.robot_pelvis_world.astype(np.float64)
+ positions: dict[int, np.ndarray] = {int(BodyJointPN.PELVIS): anchor.copy()}
+
+ _fill_torso_reference_positions(frame, positions, anchor, yaw_delta, settings)
+
+ for arm, neutral_arm, is_left, shoulder_idx, elbow_idx, wrist_idx, hand_idx in (
+ (
+ left,
+ neutral_left,
+ True,
+ BodyJointPN.LEFT_SHOULDER,
+ BodyJointPN.LEFT_ELBOW,
+ BodyJointPN.LEFT_WRIST,
+ BodyJointPN.LEFT_HAND,
+ ),
+ (
+ right,
+ neutral_right,
+ False,
+ BodyJointPN.RIGHT_SHOULDER,
+ BodyJointPN.RIGHT_ELBOW,
+ BodyJointPN.RIGHT_WRIST,
+ BodyJointPN.RIGHT_HAND,
+ ),
+ ):
+ shoulder_robot, elbow_robot, wrist_robot = _arm_fk_robot_blended(
+ arm, neutral_arm, settings, yaw_delta, is_left
+ )
+ positions[int(shoulder_idx)] = shoulder_robot
+ positions[int(elbow_idx)] = elbow_robot
+ positions[int(wrist_idx)] = wrist_robot
+ forearm = wrist_robot - elbow_robot
+ forearm_norm = float(np.linalg.norm(forearm))
+ if forearm_norm > 1e-6:
+ forearm_dir = forearm / forearm_norm
+ else:
+ forearm_dir = _map_mocap_direction_to_robot(
+ arm.wrist_world - arm.elbow_world,
+ yaw_delta,
+ settings.operator_faces_robot,
+ )
+ positions[int(hand_idx)] = wrist_robot + forearm_dir * _ROBOT_HAND_EXTENSION
+
+ spine3 = positions.get(int(BodyJointPN.SPINE3))
+ if spine3 is not None:
+ for collar_idx, shoulder_idx in (
+ (BodyJointPN.LEFT_COLLAR, BodyJointPN.LEFT_SHOULDER),
+ (BodyJointPN.RIGHT_COLLAR, BodyJointPN.RIGHT_SHOULDER),
+ ):
+ shoulder_pos = positions.get(int(shoulder_idx))
+ if shoulder_pos is not None:
+ positions[int(collar_idx)] = 0.5 * (spine3 + shoulder_pos)
+
+ return positions
+
+
+def _fill_torso_reference_positions(
+ frame: Any,
+ positions: dict[int, np.ndarray],
+ anchor: np.ndarray,
+ yaw_delta: float,
+ settings: NoitomRetargetingSettings,
+) -> None:
+ chain = (
+ BodyJointPN.PELVIS,
+ BodyJointPN.SPINE1,
+ BodyJointPN.SPINE2,
+ BodyJointPN.SPINE3,
+ BodyJointPN.NECK,
+ BodyJointPN.HEAD,
+ )
+ segment_lengths = {
+ BodyJointPN.SPINE1: _ROBOT_TORSO_SEGMENT_Z,
+ BodyJointPN.SPINE2: _ROBOT_TORSO_SEGMENT_Z,
+ BodyJointPN.SPINE3: _ROBOT_TORSO_SEGMENT_Z,
+ BodyJointPN.NECK: _ROBOT_NECK_SEGMENT,
+ BodyJointPN.HEAD: _ROBOT_HEAD_SEGMENT,
+ }
+ prev_robot = anchor.copy()
+ prev_mocap = _joint_pose(frame, BodyJointPN.PELVIS)
+ if prev_mocap is None:
+ return
+ prev_mocap_pos = prev_mocap.position.copy()
+ for joint in chain[1:]:
+ mocap_joint = _joint_pose(frame, joint)
+ if mocap_joint is None:
+ continue
+ direction = _map_mocap_direction_to_robot(
+ mocap_joint.position - prev_mocap_pos,
+ yaw_delta,
+ settings.operator_faces_robot,
+ )
+ seg_len = segment_lengths.get(joint, _ROBOT_TORSO_SEGMENT_Z)
+ robot_pos = prev_robot + direction * seg_len
+ positions[int(joint)] = robot_pos
+ prev_robot = robot_pos
+ prev_mocap_pos = mocap_joint.position.copy()
+
+
+def _wrist_pose_from_pelvis_relative(
+ wrist_rel_pelvis: np.ndarray,
+ arm_length_scale: float,
+ settings: NoitomRetargetingSettings,
+ nominal_quat_xyzw: np.ndarray,
+) -> SE3Pose:
+ anchor = settings.robot_pelvis_world.astype(np.float64)
+ offset = _map_mocap_rel_to_robot_offset(
+ wrist_rel_pelvis,
+ arm_length_scale,
+ settings.motion_scale,
+ 0.0,
+ settings.operator_faces_robot,
+ )
+ position = anchor + offset
+ quat = _normalize_quat(nominal_quat_xyzw)
+ return SE3Pose(position, quat)
+
+
+def _map_mocap_rel_to_robot_offset(
+ rel_isaac: np.ndarray,
+ arm_length_scale: float,
+ motion_scale: float,
+ yaw_delta: float,
+ operator_faces_robot: bool,
+) -> np.ndarray:
+ """Map a pelvis-relative mocap vector into robot pelvis-relative offset."""
+ rel = np.asarray(rel_isaac, dtype=np.float64) * arm_length_scale * motion_scale
+ if operator_faces_robot:
+ rel = Rotation.from_euler("z", np.pi).apply(rel)
+ return Rotation.from_euler("z", yaw_delta).apply(rel)
+
+
+def _shoulder_offset_robot(
+ settings: NoitomRetargetingSettings,
+ yaw_delta: float,
+ is_left: bool,
+) -> np.ndarray:
+ offset = (
+ settings.robot_left_shoulder_offset
+ if is_left
+ else settings.robot_right_shoulder_offset
+ )
+ return Rotation.from_euler("z", yaw_delta).apply(
+ np.asarray(offset, dtype=np.float64)
+ )
+
+
+def _clamp_reach(
+ shoulder_torso: np.ndarray,
+ target_torso: np.ndarray,
+ upper_len: float,
+ forearm_len: float,
+) -> np.ndarray:
+ offset = target_torso - shoulder_torso
+ distance = float(np.linalg.norm(offset))
+ max_reach = (upper_len + forearm_len) * 0.98
+ min_reach = abs(upper_len - forearm_len) * 1.02
+ if distance < 1e-6:
+ return shoulder_torso + np.array([max_reach * 0.5, 0.0, 0.0], dtype=np.float64)
+ clamped = float(np.clip(distance, min_reach, max_reach))
+ return shoulder_torso + offset * (clamped / distance)
+
+
+def _calibration_view_from_state(calib: _CalibrationState) -> NoitomCalibrationView:
+ return NoitomCalibrationView(
+ pelvis_world=calib.pelvis_world.copy(),
+ body_yaw_isaac=calib.body_yaw_isaac,
+ arm_length_scale=calib.arm_length_scale,
+ body_height_scale=calib.body_height_scale,
+ )
+
+
+def _calibration_view_from_scales(
+ arm_length_scale: float,
+ body_height_scale: float,
+ pelvis_world: np.ndarray,
+ body_yaw_isaac: float = 0.0,
+) -> NoitomCalibrationView:
+ return NoitomCalibrationView(
+ pelvis_world=pelvis_world.copy(),
+ body_yaw_isaac=body_yaw_isaac,
+ arm_length_scale=arm_length_scale,
+ body_height_scale=body_height_scale,
+ )
+
+
+def _aligned_skeleton_positions(
+ frame: Any,
+ settings: NoitomRetargetingSettings,
+ calib_view: NoitomCalibrationView,
+) -> dict[int, np.ndarray]:
+ from noitom_reference_draw import (
+ ReferenceSkeletonLengths,
+ aligned_reference_skeleton_from_frame,
+ )
+
+ return aligned_reference_skeleton_from_frame(
+ frame,
+ settings.robot_pelvis_world,
+ draw_scale=1.0,
+ calib_view=(None if settings.reference_use_robot_link_lengths else calib_view),
+ use_robot_link_lengths=settings.reference_use_robot_link_lengths,
+ link_lengths=ReferenceSkeletonLengths.from_retargeting_settings(settings),
+ length_scale=settings.reference_length_scale,
+ arm_length_scale=settings.reference_arm_length_scale,
+ shoulder_span_scale=settings.reference_shoulder_span_scale,
+ )
+
+
+def _shoulder_se3_from_aligned_positions(
+ positions: dict[int, np.ndarray],
+ is_left: bool,
+ nominal: SE3Pose,
+ settings: NoitomRetargetingSettings,
+) -> SE3Pose | None:
+ shoulder_index = int(
+ BodyJointPN.LEFT_SHOULDER if is_left else BodyJointPN.RIGHT_SHOULDER
+ )
+ elbow_index = int(BodyJointPN.LEFT_ELBOW if is_left else BodyJointPN.RIGHT_ELBOW)
+ shoulder = positions.get(shoulder_index)
+ if shoulder is None:
+ return None
+ elbow = positions.get(elbow_index)
+ if elbow is not None:
+ upper_arm = elbow - shoulder
+ quat = _elbow_quat_for_ik(
+ upper_arm,
+ nominal.quaternion_xyzw,
+ settings,
+ )
+ else:
+ quat = _normalize_quat(nominal.quaternion_xyzw)
+ return SE3Pose(shoulder.copy(), quat)
+
+
+def _elbow_quat_for_ik(
+ upper_arm_dir: np.ndarray,
+ nominal_quat: np.ndarray,
+ settings: NoitomRetargetingSettings,
+) -> np.ndarray:
+ return _wrist_quat_from_forearm(
+ upper_arm_dir,
+ nominal_quat,
+ settings.wrist_orientation_forearm_blend,
+ )
+
+
+def _elbow_se3_from_aligned_positions(
+ positions: dict[int, np.ndarray],
+ is_left: bool,
+ nominal: SE3Pose,
+ settings: NoitomRetargetingSettings,
+) -> SE3Pose | None:
+ elbow_index = int(BodyJointPN.LEFT_ELBOW if is_left else BodyJointPN.RIGHT_ELBOW)
+ shoulder_index = int(
+ BodyJointPN.LEFT_SHOULDER if is_left else BodyJointPN.RIGHT_SHOULDER
+ )
+ elbow = positions.get(elbow_index)
+ if elbow is None:
+ return None
+ shoulder = positions.get(shoulder_index)
+ if shoulder is not None:
+ upper_arm = elbow - shoulder
+ quat = _elbow_quat_for_ik(
+ upper_arm,
+ nominal.quaternion_xyzw,
+ settings,
+ )
+ else:
+ quat = _normalize_quat(nominal.quaternion_xyzw)
+ return SE3Pose(elbow.copy(), quat)
+
+
+def _wrist_se3_from_aligned_positions(
+ positions: dict[int, np.ndarray],
+ is_left: bool,
+ nominal: SE3Pose,
+ settings: NoitomRetargetingSettings,
+) -> SE3Pose | None:
+ wrist_index = int(BodyJointPN.LEFT_WRIST if is_left else BodyJointPN.RIGHT_WRIST)
+ elbow_index = int(BodyJointPN.LEFT_ELBOW if is_left else BodyJointPN.RIGHT_ELBOW)
+ wrist = positions.get(wrist_index)
+ if wrist is None:
+ return None
+ elbow = positions.get(elbow_index)
+ if elbow is not None:
+ forearm = wrist - elbow
+ quat = _wrist_quat_for_ik(
+ forearm,
+ nominal.quaternion_xyzw,
+ settings,
+ track_orientation=False,
+ )
+ else:
+ quat = _normalize_quat(nominal.quaternion_xyzw)
+ return SE3Pose(wrist.copy(), quat)
+
+
+def _nominal_shoulders_from_aligned_frame(
+ frame: Any,
+ settings: NoitomRetargetingSettings,
+ arm_length_scale: float,
+ body_height_scale: float,
+ pelvis_world: np.ndarray,
+) -> tuple[SE3Pose, SE3Pose] | None:
+ calib_view = _calibration_view_from_scales(
+ arm_length_scale, body_height_scale, pelvis_world
+ )
+ positions = _aligned_skeleton_positions(frame, settings, calib_view)
+ if not positions:
+ return None
+ default_left = SE3Pose.from_nominal(
+ np.zeros(3, dtype=np.float64),
+ settings.nominal_left_wrist_quat_xyzw,
+ )
+ default_right = SE3Pose.from_nominal(
+ np.zeros(3, dtype=np.float64),
+ settings.nominal_right_wrist_quat_xyzw,
+ )
+ left = _shoulder_se3_from_aligned_positions(positions, True, default_left, settings)
+ right = _shoulder_se3_from_aligned_positions(
+ positions, False, default_right, settings
+ )
+ if left is None or right is None:
+ return None
+ return left, right
+
+
+def _nominal_elbows_from_aligned_frame(
+ frame: Any,
+ settings: NoitomRetargetingSettings,
+ arm_length_scale: float,
+ body_height_scale: float,
+ pelvis_world: np.ndarray,
+) -> tuple[SE3Pose, SE3Pose] | None:
+ calib_view = _calibration_view_from_scales(
+ arm_length_scale, body_height_scale, pelvis_world
+ )
+ positions = _aligned_skeleton_positions(frame, settings, calib_view)
+ if not positions:
+ return None
+ default_left = SE3Pose.from_nominal(
+ np.zeros(3, dtype=np.float64),
+ settings.nominal_left_wrist_quat_xyzw,
+ )
+ default_right = SE3Pose.from_nominal(
+ np.zeros(3, dtype=np.float64),
+ settings.nominal_right_wrist_quat_xyzw,
+ )
+ left = _elbow_se3_from_aligned_positions(positions, True, default_left, settings)
+ right = _elbow_se3_from_aligned_positions(positions, False, default_right, settings)
+ if left is None or right is None:
+ return None
+ return left, right
+
+
+def _nominal_wrists_from_aligned_frame(
+ frame: Any,
+ settings: NoitomRetargetingSettings,
+ arm_length_scale: float,
+ body_height_scale: float,
+ pelvis_world: np.ndarray,
+) -> tuple[SE3Pose | None, SE3Pose | None]:
+ calib_view = _calibration_view_from_scales(
+ arm_length_scale, body_height_scale, pelvis_world
+ )
+ positions = _aligned_skeleton_positions(frame, settings, calib_view)
+ if not positions:
+ return None, None
+ default_left = SE3Pose.from_nominal(
+ settings.nominal_left_wrist_pos, settings.nominal_left_wrist_quat_xyzw
+ )
+ default_right = SE3Pose.from_nominal(
+ settings.nominal_right_wrist_pos, settings.nominal_right_wrist_quat_xyzw
+ )
+ return (
+ _wrist_se3_from_aligned_positions(positions, True, default_left, settings),
+ _wrist_se3_from_aligned_positions(positions, False, default_right, settings),
+ )
+
+
+def _shoulder_target_from_aligned_skeleton(
+ frame: Any,
+ calib: _CalibrationState,
+ settings: NoitomRetargetingSettings,
+ is_left: bool,
+) -> SE3Pose | None:
+ calib_view = _calibration_view_from_state(calib)
+ positions = _aligned_skeleton_positions(frame, settings, calib_view)
+ if not positions:
+ return None
+ nominal = calib.nominal_left_shoulder if is_left else calib.nominal_right_shoulder
+ return _shoulder_se3_from_aligned_positions(positions, is_left, nominal, settings)
+
+
+def _solve_shoulder_target(
+ arm: _ArmCalibration,
+ neutral: _ArmCalibration,
+ nominal: SE3Pose,
+ settings: NoitomRetargetingSettings,
+ yaw_delta: float,
+ is_left: bool,
+) -> SE3Pose:
+ shoulder_robot, _elbow_robot, _wrist_robot = _arm_fk_robot_blended(
+ arm, neutral, settings, yaw_delta, is_left
+ )
+ upper_arm = _elbow_robot - shoulder_robot
+ upper_norm = float(np.linalg.norm(upper_arm))
+ if upper_norm > 1e-6:
+ upper_dir = upper_arm / upper_norm
+ else:
+ upper_dir = _map_mocap_direction_to_robot(
+ arm.elbow_world - arm.shoulder_world,
+ yaw_delta,
+ settings.operator_faces_robot,
+ )
+ quat = _elbow_quat_for_ik(upper_dir, nominal.quaternion_xyzw, settings)
+ return SE3Pose(shoulder_robot, quat)
+
+
+def _elbow_target_from_aligned_skeleton(
+ frame: Any,
+ calib: _CalibrationState,
+ settings: NoitomRetargetingSettings,
+ is_left: bool,
+) -> SE3Pose | None:
+ calib_view = _calibration_view_from_state(calib)
+ positions = _aligned_skeleton_positions(frame, settings, calib_view)
+ if not positions:
+ return None
+ nominal = calib.nominal_left_elbow if is_left else calib.nominal_right_elbow
+ return _elbow_se3_from_aligned_positions(positions, is_left, nominal, settings)
+
+
+def _solve_elbow_target(
+ arm: _ArmCalibration,
+ neutral: _ArmCalibration,
+ nominal: SE3Pose,
+ settings: NoitomRetargetingSettings,
+ yaw_delta: float,
+ is_left: bool,
+) -> SE3Pose:
+ _shoulder_robot, elbow_robot, _wrist_robot = _arm_fk_robot_blended(
+ arm, neutral, settings, yaw_delta, is_left
+ )
+ upper_arm = elbow_robot - _shoulder_world_robot(settings, yaw_delta, is_left)
+ upper_norm = float(np.linalg.norm(upper_arm))
+ if upper_norm > 1e-6:
+ upper_dir = upper_arm / upper_norm
+ else:
+ upper_dir = _map_mocap_direction_to_robot(
+ arm.elbow_world - arm.shoulder_world,
+ yaw_delta,
+ settings.operator_faces_robot,
+ )
+ quat = _elbow_quat_for_ik(upper_dir, nominal.quaternion_xyzw, settings)
+ return SE3Pose(elbow_robot, quat)
+
+
+def _wrist_target_from_aligned_skeleton(
+ frame: Any,
+ calib: _CalibrationState,
+ settings: NoitomRetargetingSettings,
+ is_left: bool,
+) -> SE3Pose | None:
+ calib_view = _calibration_view_from_state(calib)
+ positions = _aligned_skeleton_positions(frame, settings, calib_view)
+ if not positions:
+ return None
+ nominal = calib.nominal_left if is_left else calib.nominal_right
+ return _wrist_se3_from_aligned_positions(positions, is_left, nominal, settings)
+
+
+def _solve_wrist_target(
+ torso: _TorsoFrame,
+ arm: _ArmCalibration,
+ pelvis_world: np.ndarray,
+ neutral: _ArmCalibration,
+ neutral_torso: _TorsoFrame,
+ nominal: SE3Pose,
+ calib_yaw: float,
+ arm_length_scale: float,
+ settings: NoitomRetargetingSettings,
+ is_left: bool,
+) -> SE3Pose:
+ yaw_delta = _resolve_yaw_delta(_compute_torso_yaw(torso) - calib_yaw, settings)
+
+ if settings.use_posture_based_arms:
+ shoulder_robot, elbow_robot, wrist_robot = _arm_fk_robot_blended(
+ arm, neutral, settings, yaw_delta, is_left
+ )
+ forearm = wrist_robot - elbow_robot
+ forearm_norm = float(np.linalg.norm(forearm))
+ if forearm_norm > 1e-6:
+ forearm_dir = forearm / forearm_norm
+ else:
+ forearm_dir = _map_mocap_direction_to_robot(
+ arm.wrist_world - arm.elbow_world,
+ yaw_delta,
+ settings.operator_faces_robot,
+ )
+ if settings.track_wrist_orientation:
+ wrist_world_rot = torso.rotation * arm.wrist_rot_torso
+ wrist_neutral_world_rot = neutral_torso.rotation * neutral.wrist_rot_torso
+ delta_rot_world = wrist_world_rot * wrist_neutral_world_rot.inv()
+ nominal_rot = Rotation.from_quat(nominal.quaternion_xyzw)
+ target_rot = delta_rot_world * nominal_rot
+ quat = _normalize_quat(target_rot.as_quat())
+ else:
+ quat = _wrist_quat_for_ik(
+ forearm_dir,
+ nominal.quaternion_xyzw,
+ settings,
+ track_orientation=False,
+ )
+ return SE3Pose(wrist_robot, quat)
+
+ rel_now = arm.wrist_world - pelvis_world
+ anchor = settings.robot_pelvis_world.astype(np.float64)
+ off_shoulder = _shoulder_offset_robot(settings, yaw_delta, is_left)
+ off_wrist = _map_mocap_rel_to_robot_offset(
+ rel_now,
+ arm_length_scale,
+ settings.motion_scale,
+ yaw_delta,
+ settings.operator_faces_robot,
+ )
+ clamped = _clamp_reach(
+ off_shoulder,
+ off_wrist,
+ settings.robot_upper_arm_length,
+ settings.robot_forearm_length,
+ )
+ target_pos = anchor + clamped
+
+ if settings.track_wrist_orientation:
+ wrist_world_rot = torso.rotation * arm.wrist_rot_torso
+ wrist_neutral_world_rot = neutral_torso.rotation * neutral.wrist_rot_torso
+ delta_rot_world = wrist_world_rot * wrist_neutral_world_rot.inv()
+ nominal_rot = Rotation.from_quat(nominal.quaternion_xyzw)
+ target_rot = delta_rot_world * nominal_rot
+ quat = _normalize_quat(target_rot.as_quat())
+ else:
+ quat = _normalize_quat(nominal.quaternion_xyzw)
+
+ return SE3Pose(target_pos, quat)
+
+
+def _smooth_pose(
+ current: SE3Pose,
+ target: SE3Pose,
+ position_alpha: float,
+ rotation_alpha: float,
+) -> SE3Pose:
+ pos_alpha = float(np.clip(position_alpha, 0.0, 1.0))
+ rot_alpha = float(np.clip(rotation_alpha, 0.0, 1.0))
+ position = (1.0 - pos_alpha) * current.position + pos_alpha * target.position
+ if rot_alpha <= 0.0:
+ quaternion = current.quaternion_xyzw.copy()
+ elif rot_alpha >= 1.0:
+ quaternion = target.quaternion_xyzw.copy()
+ else:
+ slerp = Slerp(
+ [0.0, 1.0],
+ Rotation.from_quat(
+ np.vstack([current.quaternion_xyzw, target.quaternion_xyzw])
+ ),
+ )
+ quaternion = _normalize_quat(slerp([rot_alpha]).as_quat()[0])
+ return SE3Pose(position, quaternion)
+
+
+__all__ = [
+ "ArmIkTargets",
+ "NoitomCalibrationView",
+ "NoitomG1Retargeter",
+ "NoitomRetargetingSettings",
+ "SE3Pose",
+ "compute_robot_reference_positions",
+ "map_point_to_robot_frame",
+ "noitom_position_to_isaac",
+ "noitom_quaternion_to_isaac",
+]
diff --git a/examples/noitom/noitom_tasks.py b/examples/noitom/noitom_tasks.py
new file mode 100644
index 000000000..33e6ce4c0
--- /dev/null
+++ b/examples/noitom/noitom_tasks.py
@@ -0,0 +1,936 @@
+# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+"""External Isaac Lab task registration for Noitom-driven G1 teleop testing."""
+
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+from typing import Any
+
+import gymnasium as gym
+import numpy as np
+from gymnasium.envs.registration import registry
+
+from isaaclab.utils.configclass import configclass
+from isaaclab_tasks.manager_based.locomanipulation.pick_place.locomanipulation_g1_env_cfg import (
+ LocomanipulationG1EnvCfg,
+)
+from isaacteleop.schema import BodyJointPN, FullBodyPosePNT, FullBodyPosePNTrackedT
+from isaacteleop.retargeting_engine.deviceio_source_nodes import (
+ DeviceIOFullBodyPosePNTracked,
+ IDeviceIOSource,
+)
+from isaacteleop.retargeting_engine.interface import (
+ ComputeContext,
+ OutputCombiner,
+ RetargeterIO,
+ RetargeterIOType,
+ TensorGroup,
+ TensorGroupType,
+)
+from isaacteleop.retargeting_engine.tensor_types import DLDataType, NDArrayType
+
+from noitom_retargeting import (
+ ArmIkTargets,
+ NoitomG1Retargeter,
+ NoitomRetargetingSettings,
+ noitom_position_to_isaac,
+)
+from noitom_reference_draw import (
+ ReferenceSkeletonLengths,
+ aligned_reference_skeleton_from_frame,
+)
+
+TASK_ID = "Isaac-PickPlace-Locomanipulation-G1-Noitom-Abs-v0"
+
+_FULL_BODY_INPUT = "deviceio_full_body"
+_ACTION_OUTPUT = "action"
+_G1_ACTION_DIM_WRIST_ONLY = 32
+_G1_ACTION_DIM_WITH_ARM_IK = 60
+_PINK_PELVIS_LINK = "g1_29dof_with_hand_rev_1_0_pelvis"
+_PINK_LEFT_ELBOW_LINK = "g1_29dof_with_hand_rev_1_0_left_elbow_link"
+_PINK_RIGHT_ELBOW_LINK = "g1_29dof_with_hand_rev_1_0_right_elbow_link"
+_PINK_LEFT_SHOULDER_LINK = "g1_29dof_with_hand_rev_1_0_left_shoulder_pitch_link"
+_PINK_RIGHT_SHOULDER_LINK = "g1_29dof_with_hand_rev_1_0_right_shoulder_pitch_link"
+_LineList = list[list[float]]
+_ColorList = list[tuple[float, float, float, float]]
+
+_LOCOMOTION_DEFAULT_HIP_HEIGHT = 0.72
+
+_NOITOM_REFERENCE_COLOR = (0.15, 0.85, 1.0, 1.0)
+_NOITOM_REFERENCE_LINE_THICKNESS = 4.0
+_NOITOM_REFERENCE_JOINT_MARKER_SIZE = 0.018
+# Approximate G1 pelvis height in the locomanipulation scene (meters, Isaac Z-up).
+_ROBOT_PELVIS_ANCHOR = (0.0, 0.0, 0.72)
+_NOITOM_REFERENCE_DEFAULT_OFFSET = (0.0, 0.0, 0.0)
+_NOITOM_WRIST_TARGET_COLOR = (1.0, 0.65, 0.1, 1.0)
+_NOITOM_WRIST_TARGET_MARKER_SIZE = 0.025
+_NOITOM_ELBOW_TARGET_COLOR = (1.0, 0.2, 0.85, 1.0)
+_NOITOM_ELBOW_TARGET_MARKER_SIZE = 0.022
+_NOITOM_SHOULDER_TARGET_COLOR = (0.2, 1.0, 0.35, 1.0)
+_NOITOM_SHOULDER_TARGET_MARKER_SIZE = 0.02
+
+
+def g1_action_dim(*, use_arm_ik_frame_tasks: bool) -> int:
+ """Flat teleop action size for the Noitom G1 locomanipulation task."""
+ return (
+ _G1_ACTION_DIM_WITH_ARM_IK
+ if use_arm_ik_frame_tasks
+ else _G1_ACTION_DIM_WRIST_ONLY
+ )
+
+
+@dataclass(frozen=True)
+class NoitomG1Settings:
+ """Release defaults for the Noitom-driven G1 locomanipulation example."""
+
+ collection_id: str = "noitom_mocap"
+ max_flatbuffer_size: int = 16 * 1024
+ teleoperation_active_default: bool = True
+ enable_motion: bool = True
+ print_period_s: float = 0.5
+ draw_reference: bool = True
+ draw_scale: float = 1.0
+ draw_offset: tuple[float, float, float] = _NOITOM_REFERENCE_DEFAULT_OFFSET
+ # Anchor the cyan skeleton to the robot pelvis instead of a fixed world offset.
+ draw_pelvis_relative: bool = True
+ draw_pelvis_anchor: tuple[float, float, float] = _ROBOT_PELVIS_ANCHOR
+ draw_wrist_targets: bool = True
+ draw_elbow_targets: bool = True
+ draw_shoulder_targets: bool = True
+ # Wrist + elbow + shoulder LocalFrameTasks for Pink IK (60D action).
+ use_arm_ik_frame_tasks: bool = True
+ retargeting: NoitomRetargetingSettings = field(
+ default_factory=lambda: NoitomRetargetingSettings(
+ robot_pelvis_world=np.array(_ROBOT_PELVIS_ANCHOR, dtype=np.float64),
+ motion_scale=0.75,
+ track_aligned_mocap_wrists=True,
+ track_elbow_ik_targets=True,
+ track_shoulder_ik_targets=True,
+ )
+ )
+
+
+_FULL_BODY_BONES = (
+ (BodyJointPN.PELVIS, BodyJointPN.SPINE1),
+ (BodyJointPN.SPINE1, BodyJointPN.SPINE2),
+ (BodyJointPN.SPINE2, BodyJointPN.SPINE3),
+ (BodyJointPN.SPINE3, BodyJointPN.NECK),
+ (BodyJointPN.NECK, BodyJointPN.HEAD),
+ (BodyJointPN.SPINE3, BodyJointPN.LEFT_COLLAR),
+ (BodyJointPN.LEFT_COLLAR, BodyJointPN.LEFT_SHOULDER),
+ (BodyJointPN.LEFT_SHOULDER, BodyJointPN.LEFT_ELBOW),
+ (BodyJointPN.LEFT_ELBOW, BodyJointPN.LEFT_WRIST),
+ (BodyJointPN.LEFT_WRIST, BodyJointPN.LEFT_HAND),
+ (BodyJointPN.SPINE3, BodyJointPN.RIGHT_COLLAR),
+ (BodyJointPN.RIGHT_COLLAR, BodyJointPN.RIGHT_SHOULDER),
+ (BodyJointPN.RIGHT_SHOULDER, BodyJointPN.RIGHT_ELBOW),
+ (BodyJointPN.RIGHT_ELBOW, BodyJointPN.RIGHT_WRIST),
+ (BodyJointPN.RIGHT_WRIST, BodyJointPN.RIGHT_HAND),
+ (BodyJointPN.PELVIS, BodyJointPN.LEFT_HIP),
+ (BodyJointPN.LEFT_HIP, BodyJointPN.LEFT_KNEE),
+ (BodyJointPN.LEFT_KNEE, BodyJointPN.LEFT_ANKLE),
+ (BodyJointPN.LEFT_ANKLE, BodyJointPN.LEFT_FOOT),
+ (BodyJointPN.PELVIS, BodyJointPN.RIGHT_HIP),
+ (BodyJointPN.RIGHT_HIP, BodyJointPN.RIGHT_KNEE),
+ (BodyJointPN.RIGHT_KNEE, BodyJointPN.RIGHT_ANKLE),
+ (BodyJointPN.RIGHT_ANKLE, BodyJointPN.RIGHT_FOOT),
+)
+DEFAULT_NOITOM_G1_SETTINGS = NoitomG1Settings()
+
+# Waist joints stay in IK; clip targets slightly inside URDF hard stops (not fixed narrow ranges).
+_WAIST_JOINT_NAMES = frozenset(
+ {"waist_yaw_joint", "waist_roll_joint", "waist_pitch_joint"}
+)
+# Inset from hard joint limits (rad). Small enough to avoid IK deadlock at the stops.
+_WAIST_HARD_LIMIT_MARGIN_RAD = 0.10
+
+
+def _build_noitom_pink_ik_action_class():
+ """Lazy import so unit tests can load noitom_tasks without Isaac Lab."""
+ import torch
+ from isaaclab.envs.mdp.actions.pink_task_space_actions import (
+ PinkInverseKinematicsAction,
+ )
+
+ class NoitomPinkInverseKinematicsAction(PinkInverseKinematicsAction):
+ """Pink IK with waist joint targets clamped to teleop-safe ranges."""
+
+ def __init__(self, cfg, env):
+ super().__init__(cfg, env)
+ clip_ids: list[int] = []
+ lows: list[float] = []
+ highs: list[float] = []
+ hard_limits = self._asset.data.joint_pos_limits.torch[0]
+ name_to_joint_idx = {
+ name: index for index, name in enumerate(self._asset.data.joint_names)
+ }
+ margin = _WAIST_HARD_LIMIT_MARGIN_RAD
+ for ik_index, name in enumerate(self._isaaclab_controlled_joint_names):
+ if name not in _WAIST_JOINT_NAMES:
+ continue
+ joint_idx = name_to_joint_idx[name]
+ lo_hard = float(hard_limits[joint_idx, 0])
+ hi_hard = float(hard_limits[joint_idx, 1])
+ lo = lo_hard + margin
+ hi = hi_hard - margin
+ if lo >= hi:
+ mid = 0.5 * (lo_hard + hi_hard)
+ half = 0.45 * (hi_hard - lo_hard)
+ lo, hi = mid - half, mid + half
+ clip_ids.append(ik_index)
+ lows.append(lo)
+ highs.append(hi)
+ self._waist_ik_indices = clip_ids
+ if clip_ids:
+ self._waist_low = torch.tensor(lows, device=self.device).view(1, -1)
+ self._waist_high = torch.tensor(highs, device=self.device).view(1, -1)
+ self._waist_ik_idx = torch.tensor(
+ clip_ids, device=self.device, dtype=torch.long
+ )
+
+ def _compute_ik_solutions(self) -> torch.Tensor:
+ sol = super()._compute_ik_solutions()
+ if self._waist_ik_indices:
+ waist = sol.index_select(1, self._waist_ik_idx)
+ sol.index_copy_(
+ 1,
+ self._waist_ik_idx,
+ torch.clamp(waist, self._waist_low, self._waist_high),
+ )
+ return sol
+
+ return NoitomPinkInverseKinematicsAction
+
+
+def _configure_noitom_pink_ik(
+ env_cfg: NoitomLocomanipulationG1EnvCfg, use_arm_frames: bool
+) -> None:
+ """Tune Pink IK for Noitom teleop; optionally add elbow/shoulder frame tasks."""
+ from isaaclab.controllers.pink_ik import LocalFrameTaskCfg, NullSpacePostureTaskCfg
+
+ env_cfg.actions.upper_body_ik.class_type = _build_noitom_pink_ik_action_class()
+ controller = env_cfg.actions.upper_body_ik.controller
+ controller.show_ik_warnings = False
+
+ if use_arm_frames:
+ tasks = list(controller.variable_input_tasks)
+ wrist_task_count = sum(
+ 1 for task in tasks if isinstance(task, LocalFrameTaskCfg)
+ )
+ arm_frame_tasks = [
+ LocalFrameTaskCfg(
+ frame=_PINK_LEFT_ELBOW_LINK,
+ base_link_frame_name=_PINK_PELVIS_LINK,
+ position_cost=9.0,
+ orientation_cost=0.0,
+ lm_damping=30.0,
+ gain=0.38,
+ ),
+ LocalFrameTaskCfg(
+ frame=_PINK_RIGHT_ELBOW_LINK,
+ base_link_frame_name=_PINK_PELVIS_LINK,
+ position_cost=9.0,
+ orientation_cost=0.0,
+ lm_damping=30.0,
+ gain=0.38,
+ ),
+ LocalFrameTaskCfg(
+ frame=_PINK_LEFT_SHOULDER_LINK,
+ base_link_frame_name=_PINK_PELVIS_LINK,
+ position_cost=6.0,
+ orientation_cost=0.0,
+ lm_damping=30.0,
+ gain=0.38,
+ ),
+ LocalFrameTaskCfg(
+ frame=_PINK_RIGHT_SHOULDER_LINK,
+ base_link_frame_name=_PINK_PELVIS_LINK,
+ position_cost=6.0,
+ orientation_cost=0.0,
+ lm_damping=30.0,
+ gain=0.38,
+ ),
+ ]
+ controller.variable_input_tasks = (
+ tasks[:wrist_task_count] + arm_frame_tasks + tasks[wrist_task_count:]
+ )
+
+ for task in controller.variable_input_tasks:
+ if isinstance(task, LocalFrameTaskCfg):
+ frame = task.frame
+ task.gain = 0.38
+ task.lm_damping = 30.0
+ task.orientation_cost = 0.0
+ if "wrist" in frame:
+ task.position_cost = 18.0
+ elif "elbow" in frame:
+ task.position_cost = 9.0
+ elif "shoulder" in frame:
+ task.position_cost = 6.0
+ else:
+ task.position_cost = 14.0
+ elif isinstance(task, NullSpacePostureTaskCfg):
+ task.cost = 0.05
+ task.gain = 0.25
+ task.lm_damping = 50.0
+
+
+def register_tasks() -> list[str]:
+ """Register the Noitom G1 locomanipulation task with Gymnasium."""
+ if TASK_ID not in registry:
+ gym.register(
+ id=TASK_ID,
+ entry_point="isaaclab.envs:ManagerBasedRLEnv",
+ kwargs={
+ "env_cfg_entry_point": ("noitom_tasks:NoitomLocomanipulationG1EnvCfg"),
+ },
+ disable_env_checker=True,
+ )
+ return []
+
+
+@configclass
+class NoitomLocomanipulationG1EnvCfg(LocomanipulationG1EnvCfg):
+ """G1 locomanipulation config using Noitom mocap as the IsaacTeleop source."""
+
+ def __post_init__(self) -> None:
+ """Use the base scene/action config and swap in the Noitom pipeline."""
+ super().__post_init__()
+ self.isaac_teleop.pipeline_builder = build_noitom_g1_locomanipulation_pipeline
+ # Pelvis fixed: agile lower-body policy otherwise crouches and breaks IK reach.
+ self.scene.robot.spawn.articulation_props.fix_root_link = True
+ # Pink IK: wrist primary, elbow/shoulder secondary frame tasks.
+ # Waist stays in IK (base G1_UPPER_BODY_IK_ACTION_CFG) but is soft-limited
+ # after solve and biased toward neutral via NullSpacePostureTask.
+ _configure_noitom_pink_ik(
+ self,
+ use_arm_frames=DEFAULT_NOITOM_G1_SETTINGS.use_arm_ik_frame_tasks,
+ )
+ self.isaac_teleop.teleoperation_active_default = (
+ DEFAULT_NOITOM_G1_SETTINGS.teleoperation_active_default
+ )
+ self.isaac_teleop.control_channel_uuid = None
+ self.isaac_teleop.app_name = "IsaacLabNoitomG1"
+
+
+def G1LocomanipulationAction(*, use_arm_ik_frame_tasks: bool = True) -> TensorGroupType:
+ """G1 locomanipulation action tensor type."""
+ return TensorGroupType(
+ "g1_locomanipulation_action",
+ [
+ NDArrayType(
+ "action",
+ shape=(g1_action_dim(use_arm_ik_frame_tasks=use_arm_ik_frame_tasks),),
+ dtype=DLDataType.FLOAT,
+ dtype_bits=32,
+ ),
+ ],
+ )
+
+
+class NoitomG1ActionSource(IDeviceIOSource):
+ """Convert Noitom mocap frames into G1 locomanipulation wrist actions."""
+
+ def __init__(
+ self,
+ name: str = "noitom_g1_action",
+ settings: NoitomG1Settings = DEFAULT_NOITOM_G1_SETTINGS,
+ ) -> None:
+ """Initialize the Noitom DeviceIO tracker and retargeter."""
+ import isaacteleop.deviceio as deviceio
+
+ self._tracker = deviceio.FullBodyTrackerPN(
+ settings.collection_id, settings.max_flatbuffer_size
+ )
+ self._collection_id = settings.collection_id
+ self._enable_motion = settings.enable_motion
+ self._print_period_s = max(0.0, settings.print_period_s)
+ self._last_print_s = 0.0
+ self._reference_viz = _NoitomReferenceVisualizer(settings)
+ self._retargeter = NoitomG1Retargeter(settings.retargeting)
+ self._use_arm_ik_frame_tasks = settings.use_arm_ik_frame_tasks
+ self._hold_targets = self._retargeter.current_arm_targets
+ self._frame_count = 0
+ self._calibration_attempts = 0
+ self._no_data_count = 0
+ self._first_frame_printed = False
+ self._calibration_fail_count = 0
+ super().__init__(name)
+
+ def get_tracker(self):
+ """Return the Noitom mocap tracker used by this source."""
+ return self._tracker
+
+ def poll_tracker(self, deviceio_session: Any) -> RetargeterIO:
+ """Poll Noitom data from the active DeviceIO session."""
+ tracked = self._tracker.get_body_pose(deviceio_session)
+ group = TensorGroup(self.input_spec()[_FULL_BODY_INPUT])
+ group[0] = tracked
+ return {_FULL_BODY_INPUT: group}
+
+ def input_spec(self) -> RetargeterIOType:
+ """Declare the raw full-body DeviceIO input."""
+ return {_FULL_BODY_INPUT: DeviceIOFullBodyPosePNTracked()}
+
+ def output_spec(self) -> RetargeterIOType:
+ """Declare the flattened G1 action output."""
+ return {
+ _ACTION_OUTPUT: G1LocomanipulationAction(
+ use_arm_ik_frame_tasks=self._use_arm_ik_frame_tasks
+ )
+ }
+
+ def _compute_fn(
+ self,
+ inputs: RetargeterIO,
+ outputs: RetargeterIO,
+ context: ComputeContext,
+ ) -> None:
+ """Convert a pushed full-body frame into the G1 action tensor."""
+ self._frame_count += 1
+
+ if context.execution_events.reset:
+ self._retargeter.clear_calibration()
+ self._calibration_attempts = 0
+ self._no_data_count = 0
+ self._calibration_fail_count = 0
+ print(
+ "NoitomG1ActionSource: cleared retargeting calibration "
+ f"collection={self._collection_id}"
+ )
+
+ # Read the raw tracked data from DeviceIO
+ tracked: FullBodyPosePNTrackedT = inputs[_FULL_BODY_INPUT][0]
+ frame: FullBodyPosePNT | None = tracked.data
+
+ # --- First-frame diagnostic ---
+ if not self._first_frame_printed:
+ self._first_frame_printed = True
+ print(
+ "NoitomG1ActionSource: first frame "
+ f"collection={self._collection_id} "
+ f"has_data={frame is not None} "
+ f"motion_enabled={self._enable_motion}"
+ )
+
+ # --- No data warning ---
+ if frame is None:
+ self._no_data_count += 1
+ if self._no_data_count == 1 or self._no_data_count % 300 == 0:
+ print(
+ f"NoitomG1ActionSource: WARNING no data from tracker "
+ f"collection={self._collection_id} "
+ f"no_data_frames={self._no_data_count}/{self._frame_count} "
+ f"(is the noitom_mocap plugin running with matching collection_id?)"
+ )
+ # Still output hold pose even without data
+ body_yaw_delta = self._retargeter.body_yaw_delta
+ action = _make_action(
+ self._hold_targets,
+ use_arm_ik_frame_tasks=self._use_arm_ik_frame_tasks,
+ )
+ outputs[_ACTION_OUTPUT][0] = np.ascontiguousarray(action, dtype=np.float32)
+ return
+
+ # Reset no-data counter when we get valid frames
+ self._no_data_count = 0
+
+ if not self._enable_motion:
+ body_yaw_delta = self._retargeter.body_yaw_delta
+ action = _make_action(
+ self._hold_targets,
+ use_arm_ik_frame_tasks=self._use_arm_ik_frame_tasks,
+ )
+ outputs[_ACTION_OUTPUT][0] = np.ascontiguousarray(action, dtype=np.float32)
+ self._reference_viz.update(frame, self._retargeter)
+ self._print_status(frame, body_yaw_delta, context)
+ return
+
+ # --- Calibration / retarget phase ---
+ if self._retargeter.awaiting_calibration:
+ self._calibration_attempts += 1
+ success = self._retargeter.calibrate(frame)
+ if success:
+ self._hold_targets = self._retargeter.current_arm_targets
+ print(
+ "NoitomG1ActionSource: calibrated neutral pose "
+ f"collection={self._collection_id} "
+ f"attempts={self._calibration_attempts}"
+ )
+ else:
+ self._calibration_fail_count += 1
+ if (
+ self._calibration_fail_count == 1
+ or self._calibration_fail_count % 150 == 0
+ ):
+ # Diagnose WHY calibration is failing
+ diag = _calibration_diagnostics(frame)
+ print(
+ f"NoitomG1ActionSource: calibration attempt "
+ f"{self._calibration_attempts} failed {diag}"
+ )
+ else:
+ result = self._retargeter.retarget(frame)
+ if result is not None:
+ self._hold_targets = result
+
+ body_yaw_delta = self._retargeter.body_yaw_delta
+ action = _make_action(
+ self._hold_targets,
+ use_arm_ik_frame_tasks=self._use_arm_ik_frame_tasks,
+ )
+ outputs[_ACTION_OUTPUT][0] = np.ascontiguousarray(action, dtype=np.float32)
+
+ self._reference_viz.update(frame, self._retargeter)
+ self._print_status(frame, body_yaw_delta, context)
+
+ def _print_status(
+ self, frame: FullBodyPosePNT, body_yaw_delta: float, context: ComputeContext
+ ) -> None:
+ if self._print_period_s <= 0.0:
+ return
+ now_s = context.graph_time.real_time_ns * 1.0e-9
+ if now_s - self._last_print_s < self._print_period_s:
+ return
+
+ self._last_print_s = now_s
+ motion = "on" if self._enable_motion else "off"
+ calib = "ready" if self._retargeter.is_calibrated else "awaiting_neutral"
+ left_pose = self._hold_targets.left_wrist.as_action_pose()
+ right_pose = self._hold_targets.right_wrist.as_action_pose()
+ frame_info = ""
+ if self._use_arm_ik_frame_tasks:
+ left_elbow_pose = self._hold_targets.left_elbow.as_action_pose()
+ right_elbow_pose = self._hold_targets.right_elbow.as_action_pose()
+ left_shoulder_pose = self._hold_targets.left_shoulder.as_action_pose()
+ right_shoulder_pose = self._hold_targets.right_shoulder.as_action_pose()
+ frame_info = (
+ f" target_left_elbow={_fmt_pose(left_elbow_pose)}"
+ f" target_right_elbow={_fmt_pose(right_elbow_pose)}"
+ f" target_left_shoulder={_fmt_pose(left_shoulder_pose)}"
+ f" target_right_shoulder={_fmt_pose(right_shoulder_pose)}"
+ )
+ print(
+ "NoitomG1ActionSource: "
+ f"joints={_valid_joint_count(frame)}/{int(BodyJointPN.NUM_JOINTS)} "
+ f"motion={motion} calibrated={calib} "
+ f"yaw_delta={body_yaw_delta:+.3f} "
+ f"motion_scale={self._retargeter.retargeting_settings.motion_scale:.2f} "
+ f"torso_yaw_influence={self._retargeter.retargeting_settings.torso_yaw_arm_influence:.2f} "
+ f"target_left={_fmt_pose(left_pose)} target_right={_fmt_pose(right_pose)}"
+ f"{frame_info} "
+ f"{_raw_full_body_status(frame)}"
+ )
+
+
+def build_noitom_g1_locomanipulation_pipeline() -> OutputCombiner:
+ """Build a one-source IsaacTeleop pipeline for Noitom G1 testing."""
+ source = NoitomG1ActionSource(settings=DEFAULT_NOITOM_G1_SETTINGS)
+ return OutputCombiner({_ACTION_OUTPUT: source.output(_ACTION_OUTPUT)})
+
+
+def _make_action(
+ targets: ArmIkTargets,
+ *,
+ use_arm_ik_frame_tasks: bool,
+) -> np.ndarray:
+ action = np.zeros(
+ g1_action_dim(use_arm_ik_frame_tasks=use_arm_ik_frame_tasks),
+ dtype=np.float32,
+ )
+ action[0:7] = targets.left_wrist.as_action_pose()
+ action[7:14] = targets.right_wrist.as_action_pose()
+ hand_offset = 14
+ if use_arm_ik_frame_tasks:
+ action[14:21] = targets.left_elbow.as_action_pose()
+ action[21:28] = targets.right_elbow.as_action_pose()
+ action[28:35] = targets.left_shoulder.as_action_pose()
+ action[35:42] = targets.right_shoulder.as_action_pose()
+ hand_offset = 42
+ action[hand_offset : hand_offset + 14] = 0.0
+ action[-4] = 0.0
+ action[-3] = 0.0
+ action[-2] = 0.0
+ action[-1] = _LOCOMOTION_DEFAULT_HIP_HEIGHT
+ return action
+
+
+class _NoitomReferenceVisualizer:
+ """Draw the incoming Noitom frame as a Kit debug-draw stick figure."""
+
+ def __init__(self, settings: NoitomG1Settings) -> None:
+ self._enabled = settings.draw_reference
+ self._draw: Any | None = None
+ self._warned = False
+ self._printed_first_draw = False
+ self._scale = settings.draw_scale
+ self._offset = np.array(settings.draw_offset, dtype=np.float32)
+ self._pelvis_relative = settings.draw_pelvis_relative
+ self._pelvis_anchor = np.array(settings.draw_pelvis_anchor, dtype=np.float32)
+ self._draw_wrist_targets = settings.draw_wrist_targets
+ self._draw_elbow_targets = (
+ settings.draw_elbow_targets and settings.use_arm_ik_frame_tasks
+ )
+ self._draw_shoulder_targets = (
+ settings.draw_shoulder_targets and settings.use_arm_ik_frame_tasks
+ )
+ self._retargeting = settings.retargeting
+
+ def update(
+ self,
+ frame: FullBodyPosePNT,
+ retargeter: NoitomG1Retargeter,
+ ) -> None:
+ if not self._enabled:
+ return
+ draw = self._get_draw_interface()
+ if draw is None:
+ return
+
+ calib_view = retargeter.calibration_view
+ draw_positions = self._reference_positions(frame, calib_view)
+ starts: list[list[float]] = []
+ ends: list[list[float]] = []
+ colors: list[tuple[float, float, float, float]] = []
+ thicknesses: list[float] = []
+
+ for parent_index, child_index in _FULL_BODY_BONES:
+ parent = draw_positions.get(int(parent_index))
+ child = draw_positions.get(int(child_index))
+ if parent is None or child is None:
+ continue
+ starts.append(parent.tolist())
+ ends.append(child.tolist())
+ colors.append(_NOITOM_REFERENCE_COLOR)
+ thicknesses.append(_NOITOM_REFERENCE_LINE_THICKNESS)
+
+ marker_starts, marker_ends, marker_colors = self._joint_markers(draw_positions)
+ starts.extend(marker_starts)
+ ends.extend(marker_ends)
+ colors.extend(marker_colors)
+ thicknesses.extend([_NOITOM_REFERENCE_LINE_THICKNESS] * len(marker_starts))
+
+ if self._draw_wrist_targets:
+ wrist_starts, wrist_ends, wrist_colors = self._frame_highlight_markers(
+ draw_positions,
+ (
+ int(BodyJointPN.LEFT_WRIST),
+ int(BodyJointPN.RIGHT_WRIST),
+ ),
+ _NOITOM_WRIST_TARGET_COLOR,
+ _NOITOM_WRIST_TARGET_MARKER_SIZE,
+ )
+ starts.extend(wrist_starts)
+ ends.extend(wrist_ends)
+ colors.extend(wrist_colors)
+ thicknesses.extend([_NOITOM_REFERENCE_LINE_THICKNESS] * len(wrist_starts))
+
+ if self._draw_elbow_targets:
+ elbow_starts, elbow_ends, elbow_colors = self._frame_highlight_markers(
+ draw_positions,
+ (
+ int(BodyJointPN.LEFT_ELBOW),
+ int(BodyJointPN.RIGHT_ELBOW),
+ ),
+ _NOITOM_ELBOW_TARGET_COLOR,
+ _NOITOM_ELBOW_TARGET_MARKER_SIZE,
+ )
+ starts.extend(elbow_starts)
+ ends.extend(elbow_ends)
+ colors.extend(elbow_colors)
+ thicknesses.extend([_NOITOM_REFERENCE_LINE_THICKNESS] * len(elbow_starts))
+
+ if self._draw_shoulder_targets:
+ shoulder_starts, shoulder_ends, shoulder_colors = (
+ self._frame_highlight_markers(
+ draw_positions,
+ (
+ int(BodyJointPN.LEFT_SHOULDER),
+ int(BodyJointPN.RIGHT_SHOULDER),
+ ),
+ _NOITOM_SHOULDER_TARGET_COLOR,
+ _NOITOM_SHOULDER_TARGET_MARKER_SIZE,
+ )
+ )
+ starts.extend(shoulder_starts)
+ ends.extend(shoulder_ends)
+ colors.extend(shoulder_colors)
+ thicknesses.extend(
+ [_NOITOM_REFERENCE_LINE_THICKNESS] * len(shoulder_starts)
+ )
+
+ draw.clear_lines()
+ if starts:
+ draw.draw_lines(starts, ends, colors, thicknesses)
+ if not self._printed_first_draw:
+ anchor = (
+ _fmt_vec(self._pelvis_anchor)
+ if self._pelvis_relative
+ else "disabled"
+ )
+ print(
+ "NoitomG1ActionSource: drawing Noitom reference skeleton "
+ f"segments={len(starts)} joints={len(draw_positions)} "
+ f"pelvis_relative={self._pelvis_relative} "
+ f"robot_pelvis_anchor={anchor}"
+ )
+ self._printed_first_draw = True
+ elif not self._warned:
+ print(
+ "NoitomG1ActionSource: reference visualizer found no drawable "
+ "Noitom bones or joints"
+ )
+ self._warned = True
+
+ def _get_draw_interface(self) -> Any | None:
+ if self._draw is not None:
+ return self._draw
+ try:
+ from isaacsim.core.experimental.utils.app import enable_extension
+
+ enable_extension("isaacsim.util.debug_draw")
+ from isaacsim.util.debug_draw import _debug_draw as omni_debug_draw
+
+ self._draw = omni_debug_draw.acquire_debug_draw_interface()
+ except (ImportError, AttributeError, RuntimeError, ModuleNotFoundError):
+ try:
+ import omni.isaac.debug_draw._debug_draw as omni_debug_draw
+
+ self._draw = omni_debug_draw.acquire_debug_draw_interface()
+ except (
+ ImportError,
+ AttributeError,
+ RuntimeError,
+ ModuleNotFoundError,
+ ) as exc:
+ if not self._warned:
+ print(
+ "NoitomG1ActionSource: reference visualizer disabled "
+ f"({type(exc).__name__}: {exc})"
+ )
+ self._warned = True
+ return None
+ except Exception as exc:
+ if not self._warned:
+ print(
+ "NoitomG1ActionSource: reference visualizer disabled "
+ f"({type(exc).__name__}: {exc})"
+ )
+ self._warned = True
+ return None
+ return self._draw
+
+ def _reference_positions(
+ self,
+ frame: FullBodyPosePNT,
+ calib_view: Any | None,
+ ) -> dict[int, np.ndarray]:
+ if self._pelvis_relative:
+ rt = self._retargeting
+ positions = aligned_reference_skeleton_from_frame(
+ frame,
+ self._pelvis_anchor,
+ draw_scale=self._scale,
+ calib_view=(
+ calib_view if not rt.reference_use_robot_link_lengths else None
+ ),
+ use_robot_link_lengths=rt.reference_use_robot_link_lengths,
+ link_lengths=ReferenceSkeletonLengths.from_retargeting_settings(rt),
+ length_scale=rt.reference_length_scale,
+ arm_length_scale=rt.reference_arm_length_scale,
+ shoulder_span_scale=rt.reference_shoulder_span_scale,
+ )
+ return {
+ index: (pos + self._offset).astype(np.float32)
+ for index, pos in positions.items()
+ }
+
+ raw_positions = _joint_position_map(frame)
+ return {
+ index: (
+ self._offset
+ + noitom_position_to_isaac(point).astype(np.float32) * self._scale
+ )
+ for index, point in raw_positions.items()
+ }
+
+ def _joint_markers(
+ self,
+ positions: dict[int, np.ndarray],
+ ) -> tuple[_LineList, _LineList, _ColorList]:
+ starts: _LineList = []
+ ends: _LineList = []
+ colors: _ColorList = []
+ marker_delta_x = np.array([_NOITOM_REFERENCE_JOINT_MARKER_SIZE, 0.0, 0.0])
+ marker_delta_y = np.array([0.0, _NOITOM_REFERENCE_JOINT_MARKER_SIZE, 0.0])
+ marker_delta_z = np.array([0.0, 0.0, _NOITOM_REFERENCE_JOINT_MARKER_SIZE])
+ for position in positions.values():
+ point = position.astype(np.float32)
+ for marker_delta in (marker_delta_x, marker_delta_y, marker_delta_z):
+ starts.append((point - marker_delta).tolist())
+ ends.append((point + marker_delta).tolist())
+ colors.append(_NOITOM_REFERENCE_COLOR)
+ return starts, ends, colors
+
+ def _frame_highlight_markers(
+ self,
+ draw_positions: dict[int, np.ndarray],
+ joint_indices: tuple[int, ...],
+ color: tuple[float, float, float, float],
+ marker_size: float,
+ ) -> tuple[_LineList, _LineList, _ColorList]:
+ """Highlight selected joints on the cyan skeleton."""
+ starts: _LineList = []
+ ends: _LineList = []
+ colors: _ColorList = []
+ marker_delta_x = np.array([marker_size, 0.0, 0.0])
+ marker_delta_y = np.array([0.0, marker_size, 0.0])
+ marker_delta_z = np.array([0.0, 0.0, marker_size])
+ for joint_index in joint_indices:
+ position = draw_positions.get(joint_index)
+ if position is None:
+ continue
+ point = position.astype(np.float32)
+ for marker_delta in (marker_delta_x, marker_delta_y, marker_delta_z):
+ starts.append((point - marker_delta).tolist())
+ ends.append((point + marker_delta).tolist())
+ colors.append(color)
+ return starts, ends, colors
+
+
+def _joint_position_map(frame: FullBodyPosePNT) -> dict[int, np.ndarray]:
+ positions: dict[int, np.ndarray] = {}
+ if frame.joints is None:
+ return positions
+ for index in range(int(BodyJointPN.NUM_JOINTS)):
+ position = _joint_position(frame, index)
+ if position is not None:
+ positions[index] = position
+ return positions
+
+
+def _joint_position(frame: FullBodyPosePNT, joint_index: int) -> np.ndarray | None:
+ if frame.joints is None:
+ return None
+ joint = frame.joints.joints(int(joint_index))
+ if not joint.is_valid:
+ return None
+ value = _point_to_array(joint.pose.position)
+ if np.all(np.isfinite(value)):
+ return value
+ return None
+
+
+def _valid_joint_count(frame: FullBodyPosePNT) -> int:
+ if frame.joints is None:
+ return 0
+ count = 0
+ for index in range(int(BodyJointPN.NUM_JOINTS)):
+ if frame.joints.joints(index).is_valid:
+ count += 1
+ return count
+
+
+def _raw_full_body_status(frame: FullBodyPosePNT) -> str:
+ left_wrist = _joint_position(frame, BodyJointPN.LEFT_WRIST)
+ right_wrist = _joint_position(frame, BodyJointPN.RIGHT_WRIST)
+ pelvis = _joint_position(frame, BodyJointPN.PELVIS)
+ spine3 = _joint_position(frame, BodyJointPN.SPINE3)
+ left_shoulder = _joint_position(frame, BodyJointPN.LEFT_SHOULDER)
+ right_shoulder = _joint_position(frame, BodyJointPN.RIGHT_SHOULDER)
+ left_elbow = _joint_position(frame, BodyJointPN.LEFT_ELBOW)
+ right_elbow = _joint_position(frame, BodyJointPN.RIGHT_ELBOW)
+
+ missing = []
+ if pelvis is None:
+ missing.append("pelvis")
+ if spine3 is None:
+ missing.append("spine3")
+ if left_shoulder is None:
+ missing.append("l_shoulder")
+ if right_shoulder is None:
+ missing.append("r_shoulder")
+ if left_elbow is None:
+ missing.append("l_elbow")
+ if right_elbow is None:
+ missing.append("r_elbow")
+ if left_wrist is None:
+ missing.append("l_wrist")
+ if right_wrist is None:
+ missing.append("r_wrist")
+
+ if missing:
+ return f"upper_body_missing={','.join(missing)}"
+ left_isaac = noitom_position_to_isaac(left_wrist)
+ right_isaac = noitom_position_to_isaac(right_wrist)
+ pelvis_isaac = noitom_position_to_isaac(pelvis)
+ return (
+ f"left_wrist={_fmt_vec(left_wrist)} right_wrist={_fmt_vec(right_wrist)} "
+ f"isaac_left={_fmt_vec(left_isaac)} isaac_right={_fmt_vec(right_isaac)} "
+ f"isaac_pelvis={_fmt_vec(pelvis_isaac)}"
+ )
+
+
+def _calibration_diagnostics(frame: FullBodyPosePNT) -> str:
+ """Detailed calibration diagnostics: which joints are valid/invalid."""
+ pelvis = _joint_position(frame, BodyJointPN.PELVIS)
+ spine3 = _joint_position(frame, BodyJointPN.SPINE3)
+ left_shoulder = _joint_position(frame, BodyJointPN.LEFT_SHOULDER)
+ right_shoulder = _joint_position(frame, BodyJointPN.RIGHT_SHOULDER)
+ left_elbow = _joint_position(frame, BodyJointPN.LEFT_ELBOW)
+ right_elbow = _joint_position(frame, BodyJointPN.RIGHT_ELBOW)
+ left_wrist = _joint_position(frame, BodyJointPN.LEFT_WRIST)
+ right_wrist = _joint_position(frame, BodyJointPN.RIGHT_WRIST)
+
+ required = {
+ "pelvis": pelvis,
+ "spine3": spine3,
+ "L_shoulder": left_shoulder,
+ "R_shoulder": right_shoulder,
+ "L_elbow": left_elbow,
+ "R_elbow": right_elbow,
+ "L_wrist": left_wrist,
+ "R_wrist": right_wrist,
+ }
+ valid = [k for k, v in required.items() if v is not None]
+ missing = [k for k, v in required.items() if v is None]
+ total_joints = int(BodyJointPN.NUM_JOINTS)
+ all_valid = sum(
+ 1
+ for i in range(total_joints)
+ if frame.joints is not None and frame.joints.joints(i).is_valid
+ )
+ return (
+ f"total_valid={all_valid}/{total_joints} "
+ f"required_valid={len(valid)}/8 "
+ f"missing=[{','.join(missing)}]"
+ if missing
+ else f"all_required_ok={valid}"
+ )
+
+
+def _point_to_array(point: Any) -> np.ndarray:
+ return np.array([point.x, point.y, point.z], dtype=np.float32)
+
+
+def _fmt_vec(vec: np.ndarray) -> str:
+ return "[" + ", ".join(f"{v:+.3f}" for v in vec) + "]"
+
+
+def _fmt_pose(pose: np.ndarray) -> str:
+ return (
+ f"pos=[{pose[0]:+.3f}, {pose[1]:+.3f}, {pose[2]:+.3f}] "
+ f"quat=[{pose[3]:+.3f}, {pose[4]:+.3f}, {pose[5]:+.3f}, {pose[6]:+.3f}]"
+ )
+
+
+__all__ = [
+ "TASK_ID",
+ "NoitomG1ActionSource",
+ "NoitomLocomanipulationG1EnvCfg",
+ "build_noitom_g1_locomanipulation_pipeline",
+ "g1_action_dim",
+ "register_tasks",
+]
diff --git a/examples/noitom/record_noitom_full_body.py b/examples/noitom/record_noitom_full_body.py
new file mode 100644
index 000000000..b19145e94
--- /dev/null
+++ b/examples/noitom/record_noitom_full_body.py
@@ -0,0 +1,122 @@
+# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+"""
+Record Noitom-published full-body data to an MCAP file.
+
+Requires ``noitom_mocap_plugin`` to be running. This records the
+``full_body_pn`` channel through ``FullBodyTrackerPN``.
+
+Usage:
+ python record_noitom_full_body.py [duration_seconds] [output.mcap]
+
+Defaults: 10 seconds -> recordings/noitom_full_body_.mcap
+"""
+
+import sys
+import time
+from datetime import datetime
+from pathlib import Path
+from typing import NamedTuple
+
+import numpy as np
+
+from isaacteleop.deviceio import (
+ DeviceIOSession,
+ FullBodyTrackerPN,
+ McapRecordingConfig,
+)
+from isaacteleop.oxr import OpenXRSession
+from isaacteleop.schema import BodyJointPN
+
+_MCAP_RECORD_REPLAY_EXAMPLE = (
+ Path(__file__).resolve().parents[1] / "mcap_record_replay" / "python"
+)
+sys.path.insert(0, str(_MCAP_RECORD_REPLAY_EXAMPLE))
+from common import BODY_JOINT_NAMES # noqa: E402
+
+
+class Args(NamedTuple):
+ duration_s: float
+ mcap_path: Path
+
+
+def _valid_array(frame: object | None) -> np.ndarray | None:
+ if frame is None or frame.joints is None:
+ return None
+
+ valid = np.zeros(int(BodyJointPN.NUM_JOINTS), dtype=np.uint8)
+ for index in range(int(BodyJointPN.NUM_JOINTS)):
+ valid[index] = 1 if frame.joints.joints(index).is_valid else 0
+ return valid
+
+
+def parse_args(argv: list[str]) -> Args:
+ args = list(argv[1:])
+ duration_option = None
+ if "--duration" in args:
+ duration_index = args.index("--duration")
+ try:
+ duration_option = float(args[duration_index + 1])
+ except IndexError:
+ raise SystemExit("[record] error: --duration requires a value") from None
+ del args[duration_index : duration_index + 2]
+
+ if duration_option is None:
+ duration_s = float(args[0]) if args else 10.0
+ output_arg = args[1] if len(args) > 1 else None
+ else:
+ duration_s = duration_option
+ output_arg = args[0] if args else None
+
+ if output_arg is not None:
+ mcap_path = Path(output_arg)
+ mcap_path.parent.mkdir(parents=True, exist_ok=True)
+ else:
+ out_dir = Path(__file__).resolve().parent / "recordings"
+ out_dir.mkdir(exist_ok=True)
+ mcap_path = out_dir / f"noitom_full_body_{datetime.now():%Y%m%d_%H%M%S}.mcap"
+
+ return Args(duration_s=duration_s, mcap_path=mcap_path)
+
+
+def main(argv: list[str]) -> int:
+ args = parse_args(argv)
+ duration_s = args.duration_s
+ mcap_path = args.mcap_path
+
+ print(f"[record] writing {mcap_path} for {duration_s:.1f}s")
+ print("[record] start noitom_mocap_plugin before running this script")
+
+ tracker = FullBodyTrackerPN()
+ required_extensions = DeviceIOSession.get_required_extensions([tracker])
+ mcap_config = McapRecordingConfig(str(mcap_path), [(tracker, "full_body_pn")])
+
+ with OpenXRSession(
+ "NoitomFullBodyRecordExample", required_extensions
+ ) as oxr_session:
+ handles = oxr_session.get_handles()
+ with DeviceIOSession.run([tracker], handles, mcap_config) as session:
+ start = time.time()
+ frame_count = 0
+ while time.time() - start < duration_s:
+ session.update()
+ frame_count += 1
+
+ if frame_count % 60 == 0:
+ tracked = tracker.get_body_pose(session)
+ valid = _valid_array(tracked.data)
+ n_valid = 0 if valid is None else int(np.count_nonzero(valid))
+ print(
+ f"[record] t={time.time() - start:5.2f}s "
+ f"frame={frame_count} "
+ f"joints={n_valid:02d}/{len(BODY_JOINT_NAMES)}"
+ )
+ time.sleep(1 / 60)
+
+ print(f"[record] done - {mcap_path}")
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main(sys.argv))
diff --git a/examples/noitom/replay_noitom_full_body.py b/examples/noitom/replay_noitom_full_body.py
new file mode 100644
index 000000000..27d7ae001
--- /dev/null
+++ b/examples/noitom/replay_noitom_full_body.py
@@ -0,0 +1,232 @@
+# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+"""
+Replay a recorded Noitom full-body MCAP file and visualize it with viser.
+
+``ReplaySession`` skips OpenXR initialization, so this runs headless on any
+machine. Open the URL viser prints (default http://localhost:8080) in a browser
+to see the body skeleton.
+
+Usage:
+ python replay_noitom_full_body.py [path/to/file.mcap] [--port 8080] [--loop]
+
+If no path is given, the newest file under ``recordings/`` is used.
+``--loop`` keeps replaying the file end-to-end until the process is killed.
+"""
+
+import argparse
+import sys
+import time
+from pathlib import Path
+
+import numpy as np
+import viser
+from mcap.reader import make_reader
+
+from isaacteleop.deviceio import FullBodyTrackerPN, McapReplayConfig, ReplaySession
+from isaacteleop.schema import BodyJointPN
+
+_MCAP_RECORD_REPLAY_EXAMPLE = (
+ Path(__file__).resolve().parents[1] / "mcap_record_replay" / "python"
+)
+sys.path.insert(0, str(_MCAP_RECORD_REPLAY_EXAMPLE))
+from common import BODY_BONES, BODY_JOINT_NAMES # noqa: E402
+
+
+def mcap_duration_s(path: Path) -> float:
+ """Read MCAP summary statistics and return wall-clock duration in seconds.
+
+ The C++ replay session does not signal end-of-file — it just logs
+ ``Replay*TrackerImpl: ... data not found`` and keeps spinning. We use
+ this duration as the stop condition so playback exits cleanly.
+ """
+ with open(path, "rb") as f:
+ reader = make_reader(f)
+ summary = reader.get_summary()
+ if summary is None or summary.statistics is None:
+ raise RuntimeError(f"{path}: MCAP file has no summary/statistics block")
+ stats = summary.statistics
+ if stats.message_count == 0:
+ return 0.0
+ return (stats.message_end_time - stats.message_start_time) / 1e9
+
+
+TRACKED_COLOR = (0.25, 0.85, 0.35)
+INVALID_COLOR = (1.0, 0.0, 0.0)
+
+
+def resolve_mcap(path_arg: str | None) -> Path:
+ if path_arg:
+ path = Path(path_arg)
+ if not path.exists():
+ sys.exit(f"[replay] error: {path} does not exist")
+ return path
+
+ recordings = Path(__file__).resolve().parent / "recordings"
+ candidates = list(recordings.glob("*.mcap"))
+ if not candidates:
+ sys.exit(
+ f"[replay] error: no .mcap files in {recordings}. "
+ "Run record_noitom_full_body.py first or pass a path."
+ )
+ return max(candidates, key=lambda p: p.stat().st_mtime)
+
+
+def _valid_bone_segments(positions: np.ndarray, valid: np.ndarray) -> np.ndarray:
+ """Return (N, 2, 3) segment array for bones whose endpoints are both valid."""
+ segments: list[np.ndarray] = []
+ for a, b in BODY_BONES:
+ if valid[a] and valid[b]:
+ segments.append(np.stack([positions[a], positions[b]], axis=0))
+ if not segments:
+ return np.zeros((0, 2, 3), dtype=np.float32)
+ return np.stack(segments, axis=0).astype(np.float32)
+
+
+def _pn_frame_arrays(
+ frame: object | None,
+) -> tuple[np.ndarray | None, np.ndarray | None]:
+ if frame is None or frame.joints is None:
+ return None, None
+
+ joint_count = int(BodyJointPN.NUM_JOINTS)
+ positions = np.zeros((joint_count, 3), dtype=np.float32)
+ valid = np.zeros(joint_count, dtype=np.uint8)
+ for index in range(joint_count):
+ joint = frame.joints.joints(index)
+ point = joint.pose.position
+ positions[index] = (point.x, point.y, point.z)
+ valid[index] = 1 if joint.is_valid else 0
+ return positions, valid
+
+
+class FullBodyViz:
+ """Viser handles (joint cloud + skeleton segments)."""
+
+ def __init__(self, server: viser.ViserServer):
+ self.color = np.array(TRACKED_COLOR, dtype=np.float32)
+ zero_pts = np.zeros((len(BODY_JOINT_NAMES), 3), dtype=np.float32)
+ zero_segs = np.zeros((0, 2, 3), dtype=np.float32)
+
+ self.points = server.scene.add_point_cloud(
+ name="/full_body/joints",
+ points=zero_pts,
+ colors=np.tile(self.color, (len(BODY_JOINT_NAMES), 1)),
+ point_size=0.01,
+ )
+ self.bones = server.scene.add_line_segments(
+ name="/full_body/bones",
+ points=zero_segs,
+ colors=np.zeros((0, 2, 3), dtype=np.float32),
+ line_width=2.0,
+ )
+
+ def update(self, positions: np.ndarray | None, valid: np.ndarray | None) -> None:
+ if positions is None or valid is None:
+ zero_pts = np.zeros((len(BODY_JOINT_NAMES), 3), dtype=np.float32)
+ self.points.points = zero_pts
+ self.points.colors = np.tile(INVALID_COLOR, (len(BODY_JOINT_NAMES), 1))
+ self.bones.points = np.zeros((0, 2, 3), dtype=np.float32)
+ self.bones.colors = np.zeros((0, 2, 3), dtype=np.float32)
+ return
+
+ positions = positions.astype(np.float32)
+ valid_bool = valid.astype(bool)
+ self.points.points = positions
+
+ point_colors = np.tile(self.color, (positions.shape[0], 1))
+ point_colors[~valid_bool] = INVALID_COLOR
+ self.points.colors = point_colors
+
+ segs = _valid_bone_segments(positions, valid_bool)
+ self.bones.points = segs
+ self.bones.colors = np.tile(self.color, (segs.shape[0], 2, 1))
+
+
+def run_once(
+ mcap_path: Path,
+ duration_s: float,
+ viz: FullBodyViz,
+ channel_name: str,
+) -> int:
+ """Play the file once for ``duration_s`` wall-clock seconds. Returns frame count."""
+ tracker = FullBodyTrackerPN()
+ replay_config = McapReplayConfig(str(mcap_path), [(tracker, channel_name)])
+
+ frames = 0
+ with ReplaySession.run(replay_config) as session:
+ start = time.time()
+ while time.time() - start < duration_s:
+ session.update()
+ tracked = tracker.get_body_pose(session)
+
+ positions, valid = _pn_frame_arrays(tracked.data)
+ if positions is None or valid is None:
+ viz.update(None, None)
+ n_valid = 0
+ else:
+ viz.update(positions, valid)
+ n_valid = int(np.count_nonzero(valid))
+
+ frames += 1
+ if frames % 60 == 0:
+ print(
+ f"[replay] t={time.time() - start:5.2f}s "
+ f"frame={frames} "
+ f"joints={n_valid:02d}/{len(BODY_JOINT_NAMES)}"
+ )
+ time.sleep(1 / 60)
+ print(f"[replay] reached end of recording after {frames} frames")
+ return frames
+
+
+def main(argv: list[str]) -> int:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("mcap", nargs="?", help="Path to .mcap file")
+ parser.add_argument(
+ "--host",
+ default="127.0.0.1",
+ help="Viser HTTP bind address (default: 127.0.0.1; pass 0.0.0.0 to expose externally)",
+ )
+ parser.add_argument("--port", type=int, default=8080, help="Viser HTTP port")
+ parser.add_argument(
+ "--loop",
+ action="store_true",
+ help="Replay the file in a loop until Ctrl+C",
+ )
+ parser.add_argument(
+ "--channel-name",
+ default="full_body_pn",
+ help="MCAP channel base name used when recording.",
+ )
+ args = parser.parse_args(argv[1:])
+
+ mcap_path = resolve_mcap(args.mcap)
+ duration_s = mcap_duration_s(mcap_path)
+
+ server = viser.ViserServer(host=args.host, port=args.port)
+ server.scene.set_up_direction("+y")
+ server.scene.add_grid(name="/grid", width=2.0, height=2.0, cell_size=0.1)
+ viz = FullBodyViz(server)
+
+ print(f"[replay] viser running at http://localhost:{args.port}")
+ print(f"[replay] reading {mcap_path} (duration {duration_s:.2f}s)")
+
+ while True:
+ run_once(mcap_path, duration_s, viz, args.channel_name)
+ if not args.loop:
+ break
+ print("[replay] looping...")
+
+ print("[replay] done - viser server still up; Ctrl+C to exit")
+ try:
+ while True:
+ time.sleep(1.0)
+ except KeyboardInterrupt:
+ pass
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main(sys.argv))
diff --git a/examples/teleop_ros2/python/teleop_ros2_node.py b/examples/teleop_ros2/python/teleop_ros2_node.py
index 8515aecd7..2fcdc3943 100755
--- a/examples/teleop_ros2/python/teleop_ros2_node.py
+++ b/examples/teleop_ros2/python/teleop_ros2_node.py
@@ -390,6 +390,7 @@ def run(self) -> int:
accept_eula=self._params.cloudxr_params.accept_eula,
setup_oob=self._params.cloudxr_params.setup_oob,
usb_local=self._params.cloudxr_params.usb_local,
+ host_client=True,
) as launcher:
self.get_logger().info(
"CloudXR runtime and WSS proxy started "
diff --git a/src/core/deviceio_base/cpp/inc/deviceio_base/full_body_tracker_pn_base.hpp b/src/core/deviceio_base/cpp/inc/deviceio_base/full_body_tracker_pn_base.hpp
new file mode 100644
index 000000000..ad4be4b2f
--- /dev/null
+++ b/src/core/deviceio_base/cpp/inc/deviceio_base/full_body_tracker_pn_base.hpp
@@ -0,0 +1,20 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+#pragma once
+
+#include "tracker.hpp"
+
+namespace core
+{
+
+struct FullBodyPosePNTrackedT;
+
+// Abstract base interface for Perception Neuron full-body tracker implementations.
+class IFullBodyTrackerPNImpl : public ITrackerImpl
+{
+public:
+ virtual const FullBodyPosePNTrackedT& get_body_pose() const = 0;
+};
+
+} // namespace core
diff --git a/src/core/deviceio_trackers/cpp/CMakeLists.txt b/src/core/deviceio_trackers/cpp/CMakeLists.txt
index 113bda804..95a96a56b 100644
--- a/src/core/deviceio_trackers/cpp/CMakeLists.txt
+++ b/src/core/deviceio_trackers/cpp/CMakeLists.txt
@@ -13,11 +13,13 @@ add_library(deviceio_trackers STATIC
joint_state_tracker.cpp
frame_metadata_tracker_oak.cpp
full_body_tracker_pico.cpp
+ full_body_tracker_pn.cpp
inc/deviceio_trackers/head_tracker.hpp
inc/deviceio_trackers/hand_tracker.hpp
inc/deviceio_trackers/controller_tracker.hpp
inc/deviceio_trackers/message_channel_tracker.hpp
inc/deviceio_trackers/full_body_tracker_pico.hpp
+ inc/deviceio_trackers/full_body_tracker_pn.hpp
inc/deviceio_trackers/generic_3axis_pedal_tracker.hpp
inc/deviceio_trackers/joint_state_tracker.hpp
inc/deviceio_trackers/frame_metadata_tracker_oak.hpp
diff --git a/src/core/deviceio_trackers/cpp/full_body_tracker_pico.cpp b/src/core/deviceio_trackers/cpp/full_body_tracker_pico.cpp
index aed4029d4..5ff4cd3b3 100644
--- a/src/core/deviceio_trackers/cpp/full_body_tracker_pico.cpp
+++ b/src/core/deviceio_trackers/cpp/full_body_tracker_pico.cpp
@@ -6,10 +6,6 @@
namespace core
{
-// ============================================================================
-// FullBodyTrackerPico Public Interface
-// ============================================================================
-
const FullBodyPosePicoTrackedT& FullBodyTrackerPico::get_body_pose(const ITrackerSession& session) const
{
return static_cast(session.get_tracker_impl(*this)).get_body_pose();
diff --git a/src/core/deviceio_trackers/cpp/full_body_tracker_pn.cpp b/src/core/deviceio_trackers/cpp/full_body_tracker_pn.cpp
new file mode 100644
index 000000000..9bdbef240
--- /dev/null
+++ b/src/core/deviceio_trackers/cpp/full_body_tracker_pn.cpp
@@ -0,0 +1,19 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+#include "inc/deviceio_trackers/full_body_tracker_pn.hpp"
+
+namespace core
+{
+
+FullBodyTrackerPN::FullBodyTrackerPN(const std::string& collection_id, size_t max_flatbuffer_size)
+ : collection_id_(collection_id), max_flatbuffer_size_(max_flatbuffer_size)
+{
+}
+
+const FullBodyPosePNTrackedT& FullBodyTrackerPN::get_body_pose(const ITrackerSession& session) const
+{
+ return static_cast(session.get_tracker_impl(*this)).get_body_pose();
+}
+
+} // namespace core
diff --git a/src/core/deviceio_trackers/cpp/inc/deviceio_trackers/full_body_tracker_pn.hpp b/src/core/deviceio_trackers/cpp/inc/deviceio_trackers/full_body_tracker_pn.hpp
new file mode 100644
index 000000000..c7e7c68c0
--- /dev/null
+++ b/src/core/deviceio_trackers/cpp/inc/deviceio_trackers/full_body_tracker_pn.hpp
@@ -0,0 +1,50 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+#pragma once
+
+#include
+#include
+
+#include
+#include
+#include
+
+namespace core
+{
+
+// SchemaTracker-backed full-body tracker for Noitom Perception Neuron samples.
+class FullBodyTrackerPN : public ITracker
+{
+public:
+ static constexpr uint32_t JOINT_COUNT = 24;
+ static constexpr size_t DEFAULT_MAX_FLATBUFFER_SIZE = 16 * 1024;
+
+ explicit FullBodyTrackerPN(const std::string& collection_id = "noitom_mocap",
+ size_t max_flatbuffer_size = DEFAULT_MAX_FLATBUFFER_SIZE);
+
+ std::string_view get_name() const override
+ {
+ return TRACKER_NAME;
+ }
+
+ const FullBodyPosePNTrackedT& get_body_pose(const ITrackerSession& session) const;
+
+ const std::string& collection_id() const
+ {
+ return collection_id_;
+ }
+
+ size_t max_flatbuffer_size() const
+ {
+ return max_flatbuffer_size_;
+ }
+
+private:
+ static constexpr const char* TRACKER_NAME = "FullBodyTrackerPN";
+
+ std::string collection_id_;
+ size_t max_flatbuffer_size_;
+};
+
+} // namespace core
diff --git a/src/core/deviceio_trackers/python/deviceio_trackers_init.py b/src/core/deviceio_trackers/python/deviceio_trackers_init.py
index 323a499a3..abd89bd80 100644
--- a/src/core/deviceio_trackers/python/deviceio_trackers_init.py
+++ b/src/core/deviceio_trackers/python/deviceio_trackers_init.py
@@ -14,6 +14,7 @@
Generic3AxisPedalTracker,
JointStateTracker,
FullBodyTrackerPico,
+ FullBodyTrackerPN,
ITrackerSession,
NUM_JOINTS,
JOINT_PALM,
@@ -28,6 +29,7 @@
"MessageChannelTracker",
"FrameMetadataTrackerOak",
"FullBodyTrackerPico",
+ "FullBodyTrackerPN",
"Generic3AxisPedalTracker",
"JointStateTracker",
"HandTracker",
diff --git a/src/core/deviceio_trackers/python/tracker_bindings.cpp b/src/core/deviceio_trackers/python/tracker_bindings.cpp
index 189cefc78..f15a5477e 100644
--- a/src/core/deviceio_trackers/python/tracker_bindings.cpp
+++ b/src/core/deviceio_trackers/python/tracker_bindings.cpp
@@ -4,6 +4,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -164,13 +165,23 @@ PYBIND11_MODULE(_deviceio_trackers, m)
py::class_>(
m, "FullBodyTrackerPico")
- .def(py::init<>())
+ .def(py::init<>(), "Construct a native PICO XR_BD_body_tracking full body tracker.")
.def(
"get_body_pose",
[](const core::FullBodyTrackerPico& self, const core::ITrackerSession& session) -> core::FullBodyPosePicoTrackedT
{ return self.get_body_pose(session); },
py::arg("session"), "Get full body pose tracked state (data is None if inactive)");
+ py::class_>(m, "FullBodyTrackerPN")
+ .def(py::init(), py::arg("collection_id") = "noitom_mocap",
+ py::arg("max_flatbuffer_size") = core::FullBodyTrackerPN::DEFAULT_MAX_FLATBUFFER_SIZE,
+ "Construct a Perception Neuron full body tracker backed by pushed FullBodyPosePN schema data.")
+ .def(
+ "get_body_pose",
+ [](const core::FullBodyTrackerPN& self, const core::ITrackerSession& session) -> core::FullBodyPosePNTrackedT
+ { return self.get_body_pose(session); },
+ py::arg("session"), "Get PN full body pose tracked state (data is None if inactive)");
+
m.attr("NUM_JOINTS") = static_cast(core::HandJoint_NUM_JOINTS);
m.attr("JOINT_PALM") = static_cast(core::HandJoint_PALM);
m.attr("JOINT_WRIST") = static_cast(core::HandJoint_WRIST);
diff --git a/src/core/live_trackers/cpp/CMakeLists.txt b/src/core/live_trackers/cpp/CMakeLists.txt
index 9f93e58df..6be4c168f 100644
--- a/src/core/live_trackers/cpp/CMakeLists.txt
+++ b/src/core/live_trackers/cpp/CMakeLists.txt
@@ -11,6 +11,7 @@ add_library(live_trackers STATIC
live_controller_tracker_impl.cpp
live_message_channel_tracker_impl.cpp
live_full_body_tracker_pico_impl.cpp
+ live_full_body_tracker_pn_impl.cpp
live_generic_3axis_pedal_tracker_impl.cpp
live_joint_state_tracker_impl.cpp
live_frame_metadata_tracker_oak_impl.cpp
@@ -22,6 +23,7 @@ add_library(live_trackers STATIC
live_controller_tracker_impl.hpp
live_message_channel_tracker_impl.hpp
live_full_body_tracker_pico_impl.hpp
+ live_full_body_tracker_pn_impl.hpp
live_generic_3axis_pedal_tracker_impl.hpp
live_joint_state_tracker_impl.hpp
live_frame_metadata_tracker_oak_impl.hpp
diff --git a/src/core/live_trackers/cpp/inc/live_trackers/live_deviceio_factory.hpp b/src/core/live_trackers/cpp/inc/live_trackers/live_deviceio_factory.hpp
index 24a78a475..1e76787a1 100644
--- a/src/core/live_trackers/cpp/inc/live_trackers/live_deviceio_factory.hpp
+++ b/src/core/live_trackers/cpp/inc/live_trackers/live_deviceio_factory.hpp
@@ -28,6 +28,8 @@ class MessageChannelTracker;
class IMessageChannelTrackerImpl;
class FullBodyTrackerPico;
class IFullBodyTrackerPicoImpl;
+class FullBodyTrackerPN;
+class IFullBodyTrackerPNImpl;
class Generic3AxisPedalTracker;
class IGeneric3AxisPedalTrackerImpl;
class JointStateTracker;
@@ -62,6 +64,7 @@ class LiveDeviceIOFactory
std::unique_ptr create_controller_tracker_impl(const ControllerTracker* tracker);
std::unique_ptr create_message_channel_tracker_impl(const MessageChannelTracker* tracker);
std::unique_ptr create_full_body_tracker_pico_impl(const FullBodyTrackerPico* tracker);
+ std::unique_ptr create_full_body_tracker_pn_impl(const FullBodyTrackerPN* tracker);
std::unique_ptr create_generic_3axis_pedal_tracker_impl(
const Generic3AxisPedalTracker* tracker);
std::unique_ptr create_joint_state_tracker_impl(const JointStateTracker* tracker);
diff --git a/src/core/live_trackers/cpp/live_deviceio_factory.cpp b/src/core/live_trackers/cpp/live_deviceio_factory.cpp
index c0b3471ba..4bb6610b0 100644
--- a/src/core/live_trackers/cpp/live_deviceio_factory.cpp
+++ b/src/core/live_trackers/cpp/live_deviceio_factory.cpp
@@ -6,6 +6,7 @@
#include "live_controller_tracker_impl.hpp"
#include "live_frame_metadata_tracker_oak_impl.hpp"
#include "live_full_body_tracker_pico_impl.hpp"
+#include "live_full_body_tracker_pn_impl.hpp"
#include "live_generic_3axis_pedal_tracker_impl.hpp"
#include "live_hand_tracker_impl.hpp"
#include "live_head_tracker_impl.hpp"
@@ -15,6 +16,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -75,6 +77,12 @@ std::unique_ptr try_create_full_body_pico_impl(LiveDeviceIOFactory
return typed ? factory.create_full_body_tracker_pico_impl(typed) : nullptr;
}
+std::unique_ptr try_create_full_body_pn_impl(LiveDeviceIOFactory& factory, const ITracker& tracker)
+{
+ auto* typed = dynamic_cast(&tracker);
+ return typed ? factory.create_full_body_tracker_pn_impl(typed) : nullptr;
+}
+
std::unique_ptr try_create_generic_pedal_impl(LiveDeviceIOFactory& factory, const ITracker& tracker)
{
auto* typed = dynamic_cast(&tracker);
@@ -109,6 +117,7 @@ inline const TrackerDispatchEntry k_tracker_dispatch[] = {
{ &try_add_extensions, &try_create_controller_impl },
{ &try_add_extensions, &try_create_message_channel_impl },
{ &try_add_extensions, &try_create_full_body_pico_impl },
+ { &try_add_extensions, &try_create_full_body_pn_impl },
{ &try_add_extensions, &try_create_generic_pedal_impl },
{ &try_add_extensions, &try_create_joint_state_impl },
{ &try_add_extensions, &try_create_oak_impl },
@@ -242,6 +251,16 @@ std::unique_ptr LiveDeviceIOFactory::create_full_body_
return std::make_unique(handles_, std::move(channels));
}
+std::unique_ptr LiveDeviceIOFactory::create_full_body_tracker_pn_impl(const FullBodyTrackerPN* tracker)
+{
+ std::unique_ptr channels;
+ if (should_record(tracker))
+ {
+ channels = LiveFullBodyTrackerPNImpl::create_mcap_channels(*writer_, get_name(tracker));
+ }
+ return std::make_unique(handles_, tracker, std::move(channels));
+}
+
std::unique_ptr LiveDeviceIOFactory::create_generic_3axis_pedal_tracker_impl(
const Generic3AxisPedalTracker* tracker)
{
diff --git a/src/core/live_trackers/cpp/live_full_body_tracker_pico_impl.hpp b/src/core/live_trackers/cpp/live_full_body_tracker_pico_impl.hpp
index 181d6b803..383fb0d97 100644
--- a/src/core/live_trackers/cpp/live_full_body_tracker_pico_impl.hpp
+++ b/src/core/live_trackers/cpp/live_full_body_tracker_pico_impl.hpp
@@ -4,6 +4,7 @@
#pragma once
#include
+#include
#include
#include
#include
diff --git a/src/core/live_trackers/cpp/live_full_body_tracker_pn_impl.cpp b/src/core/live_trackers/cpp/live_full_body_tracker_pn_impl.cpp
new file mode 100644
index 000000000..8dca39ffd
--- /dev/null
+++ b/src/core/live_trackers/cpp/live_full_body_tracker_pn_impl.cpp
@@ -0,0 +1,54 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+#include "live_full_body_tracker_pn_impl.hpp"
+
+#include
+#include
+
+namespace core
+{
+
+namespace
+{
+
+SchemaTrackerConfig make_full_body_pn_tensor_config(const FullBodyTrackerPN* tracker)
+{
+ SchemaTrackerConfig cfg;
+ cfg.collection_id = tracker->collection_id();
+ cfg.max_flatbuffer_size = tracker->max_flatbuffer_size();
+ cfg.tensor_identifier = "full_body_pn";
+ cfg.localized_name = "FullBodyTrackerPN";
+ return cfg;
+}
+
+} // namespace
+
+std::unique_ptr LiveFullBodyTrackerPNImpl::create_mcap_channels(mcap::McapWriter& writer,
+ std::string_view base_name)
+{
+ return std::make_unique(
+ writer, base_name, FullBodyPNRecordingTraits::schema_name,
+ std::vector(FullBodyPNRecordingTraits::recording_channels.begin(),
+ FullBodyPNRecordingTraits::recording_channels.end()));
+}
+
+LiveFullBodyTrackerPNImpl::LiveFullBodyTrackerPNImpl(const OpenXRSessionHandles& handles,
+ const FullBodyTrackerPN* tracker,
+ std::unique_ptr mcap_channels)
+ : mcap_channels_(std::move(mcap_channels)),
+ schema_reader_(handles, make_full_body_pn_tensor_config(tracker), mcap_channels_.get(), /*mcap_channel_index=*/0)
+{
+}
+
+void LiveFullBodyTrackerPNImpl::update(int64_t /*monotonic_time_ns*/)
+{
+ schema_reader_.update(tracked_.data);
+}
+
+const FullBodyPosePNTrackedT& LiveFullBodyTrackerPNImpl::get_body_pose() const
+{
+ return tracked_;
+}
+
+} // namespace core
diff --git a/src/core/live_trackers/cpp/live_full_body_tracker_pn_impl.hpp b/src/core/live_trackers/cpp/live_full_body_tracker_pn_impl.hpp
new file mode 100644
index 000000000..888af4b83
--- /dev/null
+++ b/src/core/live_trackers/cpp/live_full_body_tracker_pn_impl.hpp
@@ -0,0 +1,51 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+#pragma once
+
+#include "inc/live_trackers/schema_tracker.hpp"
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+
+namespace core
+{
+
+using FullBodyPNMcapChannels = McapTrackerChannels;
+using FullBodyPNSchemaTracker = SchemaTracker;
+
+class LiveFullBodyTrackerPNImpl : public IFullBodyTrackerPNImpl
+{
+public:
+ static std::vector required_extensions()
+ {
+ return SchemaTrackerBase::get_required_extensions();
+ }
+ static std::unique_ptr create_mcap_channels(mcap::McapWriter& writer,
+ std::string_view base_name);
+
+ LiveFullBodyTrackerPNImpl(const OpenXRSessionHandles& handles,
+ const FullBodyTrackerPN* tracker,
+ std::unique_ptr mcap_channels);
+
+ LiveFullBodyTrackerPNImpl(const LiveFullBodyTrackerPNImpl&) = delete;
+ LiveFullBodyTrackerPNImpl& operator=(const LiveFullBodyTrackerPNImpl&) = delete;
+ LiveFullBodyTrackerPNImpl(LiveFullBodyTrackerPNImpl&&) = delete;
+ LiveFullBodyTrackerPNImpl& operator=(LiveFullBodyTrackerPNImpl&&) = delete;
+
+ void update(int64_t monotonic_time_ns) override;
+ const FullBodyPosePNTrackedT& get_body_pose() const override;
+
+private:
+ std::unique_ptr mcap_channels_;
+ FullBodyPNSchemaTracker schema_reader_;
+ FullBodyPosePNTrackedT tracked_;
+};
+
+} // namespace core
diff --git a/src/core/mcap/cpp/inc/mcap/recording_traits.hpp b/src/core/mcap/cpp/inc/mcap/recording_traits.hpp
index cf0031e23..ce9af08e9 100644
--- a/src/core/mcap/cpp/inc/mcap/recording_traits.hpp
+++ b/src/core/mcap/cpp/inc/mcap/recording_traits.hpp
@@ -45,6 +45,13 @@ struct FullBodyPicoRecordingTraits
static constexpr std::array replay_channels = { "full_body" };
};
+struct FullBodyPNRecordingTraits
+{
+ static constexpr std::string_view schema_name = "core.FullBodyPosePNRecord";
+ static constexpr std::array recording_channels = { "full_body_pn" };
+ static constexpr std::array replay_channels = { "full_body_pn" };
+};
+
struct PedalRecordingTraits
{
static constexpr std::string_view schema_name = "core.Generic3AxisPedalOutputRecord";
diff --git a/src/core/python/deviceio_init.py b/src/core/python/deviceio_init.py
index a640a10e4..18a724c3f 100644
--- a/src/core/python/deviceio_init.py
+++ b/src/core/python/deviceio_init.py
@@ -19,6 +19,7 @@
Generic3AxisPedalTracker,
JointStateTracker,
FullBodyTrackerPico,
+ FullBodyTrackerPN,
NUM_JOINTS,
JOINT_PALM,
JOINT_WRIST,
@@ -63,6 +64,7 @@
"Generic3AxisPedalTracker",
"JointStateTracker",
"FullBodyTrackerPico",
+ "FullBodyTrackerPN",
"OpenXRSessionHandles",
"DeviceIOSession",
"McapRecordingConfig",
diff --git a/src/core/replay_trackers/cpp/CMakeLists.txt b/src/core/replay_trackers/cpp/CMakeLists.txt
index 770cc4ff3..18652f616 100644
--- a/src/core/replay_trackers/cpp/CMakeLists.txt
+++ b/src/core/replay_trackers/cpp/CMakeLists.txt
@@ -9,6 +9,7 @@ add_library(replay_trackers STATIC
replay_head_tracker_impl.cpp
replay_controller_tracker_impl.cpp
replay_full_body_tracker_pico_impl.cpp
+ replay_full_body_tracker_pn_impl.cpp
replay_generic_3axis_pedal_tracker_impl.cpp
replay_joint_state_tracker_impl.cpp
replay_message_channel_tracker_impl.cpp
@@ -17,6 +18,7 @@ add_library(replay_trackers STATIC
replay_head_tracker_impl.hpp
replay_controller_tracker_impl.hpp
replay_full_body_tracker_pico_impl.hpp
+ replay_full_body_tracker_pn_impl.hpp
replay_generic_3axis_pedal_tracker_impl.hpp
replay_joint_state_tracker_impl.hpp
replay_message_channel_tracker_impl.hpp
diff --git a/src/core/replay_trackers/cpp/inc/replay_trackers/replay_deviceio_factory.hpp b/src/core/replay_trackers/cpp/inc/replay_trackers/replay_deviceio_factory.hpp
index ddc04b063..67751fcff 100644
--- a/src/core/replay_trackers/cpp/inc/replay_trackers/replay_deviceio_factory.hpp
+++ b/src/core/replay_trackers/cpp/inc/replay_trackers/replay_deviceio_factory.hpp
@@ -19,6 +19,8 @@ class ControllerTracker;
class IControllerTrackerImpl;
class FullBodyTrackerPico;
class IFullBodyTrackerPicoImpl;
+class FullBodyTrackerPN;
+class IFullBodyTrackerPNImpl;
class Generic3AxisPedalTracker;
class IGeneric3AxisPedalTrackerImpl;
class JointStateTracker;
@@ -50,6 +52,7 @@ class ReplayDeviceIOFactory
std::unique_ptr create_hand_tracker_impl(const HandTracker* tracker);
std::unique_ptr create_controller_tracker_impl(const ControllerTracker* tracker);
std::unique_ptr create_full_body_tracker_pico_impl(const FullBodyTrackerPico* tracker);
+ std::unique_ptr create_full_body_tracker_pn_impl(const FullBodyTrackerPN* tracker);
std::unique_ptr create_generic_3axis_pedal_tracker_impl(
const Generic3AxisPedalTracker* tracker);
std::unique_ptr create_joint_state_tracker_impl(const JointStateTracker* tracker);
diff --git a/src/core/replay_trackers/cpp/replay_deviceio_factory.cpp b/src/core/replay_trackers/cpp/replay_deviceio_factory.cpp
index 4941f3a63..5c87881f2 100644
--- a/src/core/replay_trackers/cpp/replay_deviceio_factory.cpp
+++ b/src/core/replay_trackers/cpp/replay_deviceio_factory.cpp
@@ -5,6 +5,7 @@
#include "replay_controller_tracker_impl.hpp"
#include "replay_full_body_tracker_pico_impl.hpp"
+#include "replay_full_body_tracker_pn_impl.hpp"
#include "replay_generic_3axis_pedal_tracker_impl.hpp"
#include "replay_hand_tracker_impl.hpp"
#include "replay_head_tracker_impl.hpp"
@@ -13,6 +14,7 @@
#include
#include
+#include
#include
#include
#include
@@ -67,6 +69,12 @@ std::unique_ptr try_create_full_body_pico_impl(ReplayDeviceIOFacto
return typed ? factory.create_full_body_tracker_pico_impl(typed) : nullptr;
}
+std::unique_ptr try_create_full_body_pn_impl(ReplayDeviceIOFactory& factory, const ITracker& tracker)
+{
+ auto* typed = dynamic_cast(&tracker);
+ return typed ? factory.create_full_body_tracker_pn_impl(typed) : nullptr;
+}
+
std::unique_ptr try_create_generic_pedal_impl(ReplayDeviceIOFactory& factory, const ITracker& tracker)
{
auto* typed = dynamic_cast(&tracker);
@@ -88,13 +96,10 @@ std::unique_ptr try_create_message_channel_impl(ReplayDeviceIOFact
using TryCreateFn = std::unique_ptr (*)(ReplayDeviceIOFactory&, const ITracker&);
inline const TryCreateFn k_tracker_dispatch[] = {
- &try_create_head_impl,
- &try_create_hand_impl,
- &try_create_controller_impl,
- &try_create_full_body_pico_impl,
- &try_create_generic_pedal_impl,
- &try_create_joint_state_impl,
- &try_create_message_channel_impl,
+ &try_create_head_impl, &try_create_hand_impl,
+ &try_create_controller_impl, &try_create_full_body_pico_impl,
+ &try_create_full_body_pn_impl, &try_create_generic_pedal_impl,
+ &try_create_joint_state_impl, &try_create_message_channel_impl,
};
} // namespace
@@ -155,6 +160,12 @@ std::unique_ptr ReplayDeviceIOFactory::create_full_bod
return std::make_unique(open_reader(filename_), get_name(tracker));
}
+std::unique_ptr ReplayDeviceIOFactory::create_full_body_tracker_pn_impl(
+ const FullBodyTrackerPN* tracker)
+{
+ return std::make_unique(open_reader(filename_), get_name(tracker));
+}
+
std::unique_ptr ReplayDeviceIOFactory::create_generic_3axis_pedal_tracker_impl(
const Generic3AxisPedalTracker* tracker)
{
diff --git a/src/core/replay_trackers/cpp/replay_full_body_tracker_pn_impl.cpp b/src/core/replay_trackers/cpp/replay_full_body_tracker_pn_impl.cpp
new file mode 100644
index 000000000..ee6a5d2a0
--- /dev/null
+++ b/src/core/replay_trackers/cpp/replay_full_body_tracker_pn_impl.cpp
@@ -0,0 +1,44 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+#include "replay_full_body_tracker_pn_impl.hpp"
+
+#include
+#include
+#include
+
+#include
+
+namespace core
+{
+
+ReplayFullBodyTrackerPNImpl::ReplayFullBodyTrackerPNImpl(std::unique_ptr reader,
+ std::string_view base_name)
+ : mcap_viewers_(std::make_unique(
+ std::move(reader),
+ base_name,
+ std::vector(
+ FullBodyPNRecordingTraits::replay_channels.begin(), FullBodyPNRecordingTraits::replay_channels.end())))
+{
+}
+
+const FullBodyPosePNTrackedT& ReplayFullBodyTrackerPNImpl::get_body_pose() const
+{
+ return tracked_;
+}
+
+void ReplayFullBodyTrackerPNImpl::update(int64_t /*monotonic_time_ns*/)
+{
+ auto record = mcap_viewers_->read(0);
+ if (record)
+ {
+ tracked_.data = std::move(record->data);
+ }
+ else
+ {
+ std::cerr << "ReplayFullBodyTrackerPNImpl: body data not found" << std::endl;
+ tracked_.data.reset();
+ }
+}
+
+} // namespace core
diff --git a/src/core/replay_trackers/cpp/replay_full_body_tracker_pn_impl.hpp b/src/core/replay_trackers/cpp/replay_full_body_tracker_pn_impl.hpp
new file mode 100644
index 000000000..26e1bb310
--- /dev/null
+++ b/src/core/replay_trackers/cpp/replay_full_body_tracker_pn_impl.hpp
@@ -0,0 +1,37 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+#pragma once
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+namespace core
+{
+
+using FullBodyPNMcapViewers = McapTrackerViewers;
+
+class ReplayFullBodyTrackerPNImpl : public IFullBodyTrackerPNImpl
+{
+public:
+ ReplayFullBodyTrackerPNImpl(std::unique_ptr reader, std::string_view base_name);
+
+ ReplayFullBodyTrackerPNImpl(const ReplayFullBodyTrackerPNImpl&) = delete;
+ ReplayFullBodyTrackerPNImpl& operator=(const ReplayFullBodyTrackerPNImpl&) = delete;
+ ReplayFullBodyTrackerPNImpl(ReplayFullBodyTrackerPNImpl&&) = delete;
+ ReplayFullBodyTrackerPNImpl& operator=(ReplayFullBodyTrackerPNImpl&&) = delete;
+
+ void update(int64_t monotonic_time_ns) override;
+ const FullBodyPosePNTrackedT& get_body_pose() const override;
+
+private:
+ FullBodyPosePNTrackedT tracked_;
+ std::unique_ptr mcap_viewers_;
+};
+
+} // namespace core
diff --git a/src/core/retargeting_engine/python/deviceio_source_nodes/__init__.py b/src/core/retargeting_engine/python/deviceio_source_nodes/__init__.py
index 7b0a0efee..6d613cf70 100644
--- a/src/core/retargeting_engine/python/deviceio_source_nodes/__init__.py
+++ b/src/core/retargeting_engine/python/deviceio_source_nodes/__init__.py
@@ -26,12 +26,14 @@
Generic3AxisPedalOutputTrackedType,
JointStateOutputTrackedType,
FullBodyPosePicoTrackedType,
+ FullBodyPosePNTrackedType,
DeviceIOHeadPoseTracked,
DeviceIOHandPoseTracked,
DeviceIOControllerSnapshotTracked,
DeviceIOGeneric3AxisPedalOutputTracked,
DeviceIOJointStateOutputTracked,
DeviceIOFullBodyPosePicoTracked,
+ DeviceIOFullBodyPosePNTracked,
MessageChannelMessagesTrackedType,
MessageChannelConnectionStatus,
MessageChannelStatusType,
@@ -61,6 +63,7 @@
"Generic3AxisPedalOutputTrackedType",
"JointStateOutputTrackedType",
"FullBodyPosePicoTrackedType",
+ "FullBodyPosePNTrackedType",
"MessageChannelMessagesTrackedType",
"MessageChannelConnectionStatus",
"MessageChannelStatusType",
@@ -70,6 +73,7 @@
"DeviceIOGeneric3AxisPedalOutputTracked",
"DeviceIOJointStateOutputTracked",
"DeviceIOFullBodyPosePicoTracked",
+ "DeviceIOFullBodyPosePNTracked",
"DeviceIOMessageChannelMessagesTracked",
"MessageChannelMessagesTrackedGroup",
"MessageChannelStatusGroup",
diff --git a/src/core/retargeting_engine/python/deviceio_source_nodes/deviceio_tensor_types.py b/src/core/retargeting_engine/python/deviceio_source_nodes/deviceio_tensor_types.py
index 89f92b5db..c95cb7cb3 100644
--- a/src/core/retargeting_engine/python/deviceio_source_nodes/deviceio_tensor_types.py
+++ b/src/core/retargeting_engine/python/deviceio_source_nodes/deviceio_tensor_types.py
@@ -20,6 +20,7 @@
Generic3AxisPedalOutputTrackedT,
JointStateOutputTrackedT,
FullBodyPosePicoTrackedT,
+ FullBodyPosePNTrackedT,
MessageChannelMessagesTrackedT,
)
@@ -140,6 +141,26 @@ def validate_value(self, value: Any) -> None:
)
+class FullBodyPosePNTrackedType(TensorType):
+ """FullBodyPosePNTrackedT wrapper type from DeviceIO FullBodyTrackerPN."""
+
+ def __init__(self, name: str) -> None:
+ super().__init__(name)
+
+ def _check_instance_compatibility(self, other: TensorType) -> bool:
+ if not isinstance(other, FullBodyPosePNTrackedType):
+ raise TypeError(
+ f"Expected FullBodyPosePNTrackedType, got {type(other).__name__}"
+ )
+ return True
+
+ def validate_value(self, value: Any) -> None:
+ if not isinstance(value, FullBodyPosePNTrackedT):
+ raise TypeError(
+ f"Expected FullBodyPosePNTrackedT for '{self.name}', got {type(value).__name__}"
+ )
+
+
class MessageChannelMessagesTrackedType(TensorType):
"""MessageChannelMessagesTrackedT wrapper type from DeviceIO MessageChannelTracker."""
@@ -256,6 +277,18 @@ def DeviceIOFullBodyPosePicoTracked() -> TensorGroupType:
)
+def DeviceIOFullBodyPosePNTracked() -> TensorGroupType:
+ """Tracked full body pose data from DeviceIO FullBodyTrackerPN.
+
+ Contains:
+ full_body_tracked: FullBodyPosePNTrackedT wrapper (always set; .data is None when inactive)
+ """
+ return TensorGroupType(
+ "deviceio_full_body_pose_pn",
+ [FullBodyPosePNTrackedType("full_body_tracked")],
+ )
+
+
def DeviceIOMessageChannelMessagesTracked() -> TensorGroupType:
"""Tracked message wrapper from DeviceIO MessageChannelTracker."""
return TensorGroupType(
diff --git a/src/core/schema/fbs/full_body_pn.fbs b/src/core/schema/fbs/full_body_pn.fbs
new file mode 100644
index 000000000..93a192c58
--- /dev/null
+++ b/src/core/schema/fbs/full_body_pn.fbs
@@ -0,0 +1,64 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+include "full_body.fbs";
+include "timestamp.fbs";
+
+namespace core;
+
+// Body joint indices for Noitom Perception Neuron full-body mocap samples.
+// The order intentionally mirrors the 24 slots consumed by the Noitom G1 example.
+enum BodyJointPN : uint8 {
+ PELVIS = 0,
+ LEFT_HIP = 1,
+ RIGHT_HIP = 2,
+ SPINE1 = 3,
+ LEFT_KNEE = 4,
+ RIGHT_KNEE = 5,
+ SPINE2 = 6,
+ LEFT_ANKLE = 7,
+ RIGHT_ANKLE = 8,
+ SPINE3 = 9,
+ LEFT_FOOT = 10,
+ RIGHT_FOOT = 11,
+ NECK = 12,
+ LEFT_COLLAR = 13,
+ RIGHT_COLLAR = 14,
+ HEAD = 15,
+ LEFT_SHOULDER = 16,
+ RIGHT_SHOULDER = 17,
+ LEFT_ELBOW = 18,
+ RIGHT_ELBOW = 19,
+ LEFT_WRIST = 20,
+ RIGHT_WRIST = 21,
+ LEFT_HAND = 22,
+ RIGHT_HAND = 23,
+
+ NUM_JOINTS = 24,
+}
+
+// Fixed-size array of 24 Perception Neuron body joint locations.
+struct BodyJointsPN {
+ joints: [BodyJointPose:24];
+}
+
+// Full body pose data pushed by the Noitom/Perception Neuron plugin.
+table FullBodyPosePN {
+ joints: BodyJointsPN (id: 0);
+
+ // Quality flag only. When false, individual joint is_valid flags should be consulted.
+ all_joint_poses_tracked: bool (id: 1);
+}
+
+// Tracked wrapper for the in-memory tracker API (data is null when no PN sample is available).
+table FullBodyPosePNTracked {
+ data: FullBodyPosePN (id: 0);
+}
+
+// MCAP recording wrapper for FullBodyPosePN.
+table FullBodyPosePNRecord {
+ data: FullBodyPosePN (id: 0);
+ timestamp: DeviceDataTimestamp (id: 1);
+}
+
+root_type FullBodyPosePNRecord;
diff --git a/src/core/schema/python/CMakeLists.txt b/src/core/schema/python/CMakeLists.txt
index 4d02a4e8d..25484d24f 100644
--- a/src/core/schema/python/CMakeLists.txt
+++ b/src/core/schema/python/CMakeLists.txt
@@ -5,6 +5,7 @@ pybind11_add_module(schema_py
oak_bindings.h
controller_bindings.h
full_body_bindings.h
+ full_body_pn_bindings.h
hand_bindings.h
head_bindings.h
joint_state_bindings.h
diff --git a/src/core/schema/python/full_body_pn_bindings.h b/src/core/schema/python/full_body_pn_bindings.h
new file mode 100644
index 000000000..8010dccef
--- /dev/null
+++ b/src/core/schema/python/full_body_pn_bindings.h
@@ -0,0 +1,127 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+// Python bindings for the FullBodyPosePN FlatBuffer schema.
+
+#pragma once
+
+#include
+#include
+#include
+
+#include
+#include
+
+namespace py = pybind11;
+
+namespace core
+{
+
+inline void bind_full_body_pn(py::module& m)
+{
+ py::enum_(m, "BodyJointPN")
+ .value("PELVIS", BodyJointPN_PELVIS)
+ .value("LEFT_HIP", BodyJointPN_LEFT_HIP)
+ .value("RIGHT_HIP", BodyJointPN_RIGHT_HIP)
+ .value("SPINE1", BodyJointPN_SPINE1)
+ .value("LEFT_KNEE", BodyJointPN_LEFT_KNEE)
+ .value("RIGHT_KNEE", BodyJointPN_RIGHT_KNEE)
+ .value("SPINE2", BodyJointPN_SPINE2)
+ .value("LEFT_ANKLE", BodyJointPN_LEFT_ANKLE)
+ .value("RIGHT_ANKLE", BodyJointPN_RIGHT_ANKLE)
+ .value("SPINE3", BodyJointPN_SPINE3)
+ .value("LEFT_FOOT", BodyJointPN_LEFT_FOOT)
+ .value("RIGHT_FOOT", BodyJointPN_RIGHT_FOOT)
+ .value("NECK", BodyJointPN_NECK)
+ .value("LEFT_COLLAR", BodyJointPN_LEFT_COLLAR)
+ .value("RIGHT_COLLAR", BodyJointPN_RIGHT_COLLAR)
+ .value("HEAD", BodyJointPN_HEAD)
+ .value("LEFT_SHOULDER", BodyJointPN_LEFT_SHOULDER)
+ .value("RIGHT_SHOULDER", BodyJointPN_RIGHT_SHOULDER)
+ .value("LEFT_ELBOW", BodyJointPN_LEFT_ELBOW)
+ .value("RIGHT_ELBOW", BodyJointPN_RIGHT_ELBOW)
+ .value("LEFT_WRIST", BodyJointPN_LEFT_WRIST)
+ .value("RIGHT_WRIST", BodyJointPN_RIGHT_WRIST)
+ .value("LEFT_HAND", BodyJointPN_LEFT_HAND)
+ .value("RIGHT_HAND", BodyJointPN_RIGHT_HAND)
+ .value("NUM_JOINTS", BodyJointPN_NUM_JOINTS);
+
+ py::class_(m, "BodyJointsPN")
+ .def(py::init<>())
+ .def(
+ "joints",
+ [](const BodyJointsPN& self, size_t index) -> const BodyJointPose*
+ {
+ if (index >= static_cast(BodyJointPN_NUM_JOINTS))
+ {
+ throw py::index_error("BodyJointsPN index out of range (must be 0-23)");
+ }
+ return (*self.joints())[index];
+ },
+ py::arg("index"), py::return_value_policy::reference_internal,
+ "Get the BodyJointPose at the specified index (0 to NUM_JOINTS-1).")
+ .def("__repr__", [](const BodyJointsPN&) { return "BodyJointsPN(joints=[...24 BodyJointPose entries...])"; });
+
+ py::class_>(m, "FullBodyPosePNT")
+ .def(py::init(
+ []()
+ {
+ auto obj = std::make_shared();
+ obj->joints = std::make_shared();
+ return obj;
+ }))
+ .def(py::init(
+ [](const BodyJointsPN& joints)
+ {
+ auto obj = std::make_shared();
+ obj->joints = std::make_shared(joints);
+ return obj;
+ }),
+ py::arg("joints"))
+ .def_property_readonly(
+ "joints", [](const FullBodyPosePNT& self) -> const BodyJointsPN* { return self.joints.get(); },
+ py::return_value_policy::reference_internal)
+ .def_readwrite("all_joint_poses_tracked", &FullBodyPosePNT::all_joint_poses_tracked)
+ .def("__repr__",
+ [](const FullBodyPosePNT& self)
+ {
+ std::string joints_str = self.joints ? "BodyJointsPN(joints=[...24 entries...])" : "None";
+ return "FullBodyPosePNT(joints=" + joints_str + ")";
+ });
+
+ py::class_>(m, "FullBodyPosePNRecord")
+ .def(py::init<>())
+ .def(py::init(
+ [](const FullBodyPosePNT& data, const DeviceDataTimestamp& timestamp)
+ {
+ auto obj = std::make_shared();
+ obj->data = std::make_shared(data);
+ obj->timestamp = std::make_shared(timestamp);
+ return obj;
+ }),
+ py::arg("data"), py::arg("timestamp"))
+ .def_property_readonly(
+ "data", [](const FullBodyPosePNRecordT& self) -> std::shared_ptr { return self.data; })
+ .def_readonly("timestamp", &FullBodyPosePNRecordT::timestamp)
+ .def("__repr__", [](const FullBodyPosePNRecordT& self)
+ { return "FullBodyPosePNRecord(data=" + std::string(self.data ? "FullBodyPosePNT(...)" : "None") + ")"; });
+
+ py::class_>(m, "FullBodyPosePNTrackedT")
+ .def(py::init<>())
+ .def(py::init(
+ [](const FullBodyPosePNT& data)
+ {
+ auto obj = std::make_shared();
+ obj->data = std::make_shared(data);
+ return obj;
+ }),
+ py::arg("data"))
+ .def_property_readonly(
+ "data", [](const FullBodyPosePNTrackedT& self) -> std::shared_ptr { return self.data; })
+ .def("__repr__",
+ [](const FullBodyPosePNTrackedT& self) {
+ return std::string("FullBodyPosePNTrackedT(data=") + (self.data ? "FullBodyPosePNT(...)" : "None") + ")";
+ });
+}
+
+} // namespace core
diff --git a/src/core/schema/python/schema_init.py b/src/core/schema/python/schema_init.py
index 478847dad..be5acddf3 100644
--- a/src/core/schema/python/schema_init.py
+++ b/src/core/schema/python/schema_init.py
@@ -51,11 +51,16 @@
FrameMetadataOakRecord,
# Full body-related types.
BodyJointPico,
+ BodyJointPN,
BodyJointPose,
BodyJointsPico,
+ BodyJointsPN,
FullBodyPosePicoT,
FullBodyPosePicoTrackedT,
FullBodyPosePicoRecord,
+ FullBodyPosePNT,
+ FullBodyPosePNTrackedT,
+ FullBodyPosePNRecord,
)
@@ -105,7 +110,12 @@
"BodyJointPose",
"BodyJointsPico",
"BodyJointPico",
+ "BodyJointPN",
+ "BodyJointsPN",
"FullBodyPosePicoT",
"FullBodyPosePicoTrackedT",
"FullBodyPosePicoRecord",
+ "FullBodyPosePNT",
+ "FullBodyPosePNTrackedT",
+ "FullBodyPosePNRecord",
]
diff --git a/src/core/schema/python/schema_module.cpp b/src/core/schema/python/schema_module.cpp
index b08e3bfcd..7afe56365 100644
--- a/src/core/schema/python/schema_module.cpp
+++ b/src/core/schema/python/schema_module.cpp
@@ -8,6 +8,7 @@
// Include binding definitions.
#include "controller_bindings.h"
#include "full_body_bindings.h"
+#include "full_body_pn_bindings.h"
#include "hand_bindings.h"
#include "head_bindings.h"
#include "joint_state_bindings.h"
@@ -52,4 +53,7 @@ PYBIND11_MODULE(_schema, m)
// Bind full body types (BodyJointPose, BodyJointsPico structs, FullBodyPosePicoT table).
core::bind_full_body(m);
+
+ // Bind Perception Neuron full body types.
+ core::bind_full_body_pn(m);
}
diff --git a/src/core/schema_tests/cpp/test_full_body.cpp b/src/core/schema_tests/cpp/test_full_body.cpp
index 614d94a43..dd9025318 100644
--- a/src/core/schema_tests/cpp/test_full_body.cpp
+++ b/src/core/schema_tests/cpp/test_full_body.cpp
@@ -9,6 +9,7 @@
// Include generated FlatBuffer headers.
#include
+#include
#include
#include
@@ -51,6 +52,9 @@ static_assert(sizeof(core::BodyJointsPico) == 24 * sizeof(core::BodyJointPose),
static_assert(core::BodyJointPico_PELVIS == 0, "PELVIS should be index 0");
static_assert(core::BodyJointPico_RIGHT_HAND == 23, "RIGHT_HAND should be index 23");
static_assert(core::BodyJointPico_NUM_JOINTS == 24, "NUM_JOINTS should be 24");
+static_assert(core::BodyJointPN_PELVIS == 0, "PN PELVIS should be index 0");
+static_assert(core::BodyJointPN_RIGHT_HAND == 23, "PN RIGHT_HAND should be index 23");
+static_assert(core::BodyJointPN_NUM_JOINTS == 24, "PN NUM_JOINTS should be 24");
// =============================================================================
// BodyJointPose Tests
@@ -404,3 +408,23 @@ TEST_CASE("FullBodyPosePicoRecord can be unpacked with DeviceDataTimestamp", "[f
CHECK(unpacked->timestamp->sample_time_raw_device_clock() == 333LL);
CHECK(unpacked->data->joints->joints()->size() == static_cast(core::BodyJointPico_NUM_JOINTS));
}
+
+TEST_CASE("FullBodyPosePNRecord serialization with DeviceDataTimestamp", "[full_body_pn][flatbuffers]")
+{
+ flatbuffers::FlatBufferBuilder builder(4096);
+
+ auto record = std::make_shared();
+ record->data = std::make_shared();
+ record->data->joints = std::make_unique();
+ record->timestamp = std::make_shared(10LL, 20LL, 30LL);
+
+ auto offset = core::FullBodyPosePNRecord::Pack(builder, record.get());
+ builder.Finish(offset);
+
+ auto deserialized = flatbuffers::GetRoot(builder.GetBufferPointer());
+
+ CHECK(deserialized->timestamp()->available_time_local_common_clock() == 10LL);
+ CHECK(deserialized->timestamp()->sample_time_local_common_clock() == 20LL);
+ CHECK(deserialized->timestamp()->sample_time_raw_device_clock() == 30LL);
+ CHECK(deserialized->data()->joints()->joints()->size() == static_cast(core::BodyJointPN_NUM_JOINTS));
+}
diff --git a/src/core/schema_tests/python/test_full_body.py b/src/core/schema_tests/python/test_full_body.py
index a1a7f32ed..43f3dcd2f 100644
--- a/src/core/schema_tests/python/test_full_body.py
+++ b/src/core/schema_tests/python/test_full_body.py
@@ -26,9 +26,13 @@
from isaacteleop.schema import (
FullBodyPosePicoT,
FullBodyPosePicoRecord,
+ FullBodyPosePNT,
+ FullBodyPosePNRecord,
BodyJointsPico,
+ BodyJointsPN,
BodyJointPose,
BodyJointPico,
+ BodyJointPN,
Pose,
Point,
Quaternion,
@@ -285,3 +289,35 @@ def test_timestamp_fields(self):
assert record.timestamp.available_time_local_common_clock == 111
assert record.timestamp.sample_time_local_common_clock == 222
assert record.timestamp.sample_time_raw_device_clock == 333
+
+
+class TestFullBodyPosePNSchema:
+ """Tests for Perception Neuron full-body schema bindings."""
+
+ def test_joint_values_match_expected_slots(self):
+ """Test that BodyJointPN preserves the 24-slot Noitom full-body layout."""
+ assert int(BodyJointPN.PELVIS) == 0
+ assert int(BodyJointPN.LEFT_WRIST) == 20
+ assert int(BodyJointPN.RIGHT_WRIST) == 21
+ assert int(BodyJointPN.LEFT_HAND) == 22
+ assert int(BodyJointPN.RIGHT_HAND) == 23
+ assert int(BodyJointPN.NUM_JOINTS) == 24
+
+ def test_default_construction(self):
+ """Test default construction creates PN pose data with fixed joints."""
+ body_pose = FullBodyPosePNT()
+
+ assert body_pose is not None
+ assert body_pose.joints is not None
+ assert body_pose.joints.joints(int(BodyJointPN.PELVIS)) is not None
+
+ def test_record_timestamp(self):
+ """Test FullBodyPosePNRecord carries DeviceDataTimestamp."""
+ data = FullBodyPosePNT(BodyJointsPN())
+ ts = DeviceDataTimestamp(10, 20, 30)
+ record = FullBodyPosePNRecord(data, ts)
+
+ assert record.data is not None
+ assert record.timestamp.available_time_local_common_clock == 10
+ assert record.timestamp.sample_time_local_common_clock == 20
+ assert record.timestamp.sample_time_raw_device_clock == 30
diff --git a/src/plugins/noitom_mocap/CMakeLists.txt b/src/plugins/noitom_mocap/CMakeLists.txt
new file mode 100644
index 000000000..850a9b913
--- /dev/null
+++ b/src/plugins/noitom_mocap/CMakeLists.txt
@@ -0,0 +1,57 @@
+# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux")
+ message(STATUS "Skipping noitom_mocap plugin (Linux only)")
+ add_custom_target(noitom_mocap_plugin
+ COMMAND ${CMAKE_COMMAND} -E echo "Skipping noitom_mocap: Linux only")
+ return()
+endif()
+
+set(NOITOM_MOCAP_API_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/third_party/mocap_api"
+ CACHE PATH "Path to the bundled Noitom MocapApi SDK root")
+
+if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(aarch64|arm64)$")
+ set(_NOITOM_MOCAP_LIB_DIR "${NOITOM_MOCAP_API_ROOT}/lib/arm64")
+else()
+ set(_NOITOM_MOCAP_LIB_DIR "${NOITOM_MOCAP_API_ROOT}/lib/x86_64")
+endif()
+set(_NOITOM_MOCAP_LIB "${_NOITOM_MOCAP_LIB_DIR}/libMocapApi.so")
+set(_NOITOM_MOCAP_INCLUDE_DIR "${NOITOM_MOCAP_API_ROOT}/include")
+
+if(NOT EXISTS "${_NOITOM_MOCAP_LIB}")
+ message(FATAL_ERROR "Noitom Mocap SDK library not found: ${_NOITOM_MOCAP_LIB}")
+endif()
+if(NOT EXISTS "${_NOITOM_MOCAP_INCLUDE_DIR}/MocapApi/MocapApi.h")
+ message(FATAL_ERROR "Noitom Mocap SDK headers not found under: ${_NOITOM_MOCAP_INCLUDE_DIR}")
+endif()
+
+add_executable(noitom_mocap_plugin
+ main.cpp
+ noitom_mocap_plugin.cpp
+)
+
+target_include_directories(noitom_mocap_plugin
+ PRIVATE
+ "${_NOITOM_MOCAP_INCLUDE_DIR}"
+)
+
+target_link_libraries(noitom_mocap_plugin PRIVATE
+ "${_NOITOM_MOCAP_LIB}"
+ pusherio::pusherio
+ oxr::oxr_core
+ isaacteleop_schema
+)
+
+set_target_properties(noitom_mocap_plugin PROPERTIES
+ INSTALL_RPATH "$ORIGIN"
+)
+
+install(TARGETS noitom_mocap_plugin RUNTIME DESTINATION plugins/noitom_mocap)
+install(FILES plugin.yaml README.md DESTINATION plugins/noitom_mocap)
+install(
+ FILES tools/noitom_mocap_printer.py
+ DESTINATION plugins/noitom_mocap/tools
+ PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE
+)
+install(FILES "${_NOITOM_MOCAP_LIB}" DESTINATION plugins/noitom_mocap)
diff --git a/src/plugins/noitom_mocap/README.md b/src/plugins/noitom_mocap/README.md
new file mode 100644
index 000000000..8fde68902
--- /dev/null
+++ b/src/plugins/noitom_mocap/README.md
@@ -0,0 +1,71 @@
+
+
+# Noitom Mocap Plugin
+
+Optional plugin that reads Noitom Hybrid Data Server data through the bundled
+MocapApi SDK and publishes IsaacTeleop full-body samples over OpenXR tensor
+data.
+
+The plugin publishes the Perception Neuron schema in
+`src/core/schema/fbs/full_body_pn.fbs`. Each Noitom avatar update is converted
+to `FullBodyPosePN`, pushed on tensor identifier `full_body_pn`, and consumed
+with `FullBodyTrackerPN("noitom_mocap")`.
+
+Positions are read from the Noitom SDK in centimeters and published in meters.
+MCAP timestamps use the local monotonic clock for the local/common time fields
+and the Noitom avatar PTP timestamp for the raw device clock when the SDK
+provides it.
+
+Build with:
+
+```bash
+cmake -B build -DISAAC_TELEOP_PYTHON_VERSION=3.12 -DBUILD_PLUGIN_NOITOM_MOCAP=ON
+cmake --build build --target noitom_mocap_plugin --parallel
+cmake --install build
+```
+
+## Run the Pusher
+
+Start CloudXR/OpenXR first, keep Axis Studio or the Noitom Data Server
+running (TCP mode), then launch the plugin.
+
+**Before starting the plugin on Linux**, verify the Windows HDS port is open:
+
+```bash
+nc -zv 8001
+```
+
+`Connection refused` means HDS is not listening yet — fix Windows broadcast first.
+
+```bash
+./install/plugins/noitom_mocap/noitom_mocap_plugin --host=127.0.0.1 --port=8001
+```
+
+The default collection id is `noitom_mocap`. Override it only if the consumer
+uses the same value:
+
+```bash
+./install/plugins/noitom_mocap/noitom_mocap_plugin \
+ --host= \
+ --port= \
+ --collection-id=noitom_mocap
+```
+
+In a second terminal, print frames received through DeviceIO:
+
+```bash
+python src/plugins/noitom_mocap/tools/noitom_mocap_printer.py --duration=10
+```
+
+## Record and Replay
+
+```bash
+cmake --build build --target python_package noitom_mocap_plugin --parallel
+
+python examples/noitom/record_noitom_full_body.py --duration 10
+
+python examples/noitom/replay_noitom_full_body.py
+```
diff --git a/src/plugins/noitom_mocap/main.cpp b/src/plugins/noitom_mocap/main.cpp
new file mode 100644
index 000000000..97449dff7
--- /dev/null
+++ b/src/plugins/noitom_mocap/main.cpp
@@ -0,0 +1,171 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+#include "noitom_mocap_plugin.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+using namespace plugins::noitom_mocap;
+
+namespace
+{
+
+std::atomic g_stop_requested{ false };
+
+void signal_handler(int signal)
+{
+ if (signal == SIGINT || signal == SIGTERM)
+ {
+ g_stop_requested.store(true, std::memory_order_relaxed);
+ }
+}
+
+void print_usage(const char* program_name)
+{
+ std::cout << "Usage: " << program_name << " [options]\n"
+ << "\nConnection:\n"
+ << " --protocol=tcp|udp Noitom HDS connection mode (default: tcp)\n"
+ << " --host=ADDR TCP server address (default: 127.0.0.1)\n"
+ << " --port=N TCP server port (default: 8001)\n"
+ << " --udp-local-port=N UDP local listen port (default: 8002)\n"
+ << " --udp-server-host=ADDR Optional UDP server address\n"
+ << " --udp-server-port=N Optional UDP server port (default: 8001)\n"
+ << "\nDeviceIO:\n"
+ << " --collection-id=ID Tensor collection id (default: noitom_mocap)\n"
+ << " --max-flatbuffer-size=N Max serialized frame size (default: "
+ << DEFAULT_NOITOM_MOCAP_MAX_FLATBUFFER_SIZE << ")\n"
+ << " --rate=N Poll loop rate in Hz (default: 90)\n"
+ << "\nGeneral:\n"
+ << " --help Show this message\n";
+}
+
+uint16_t parse_u16(const std::string& value, const std::string& name)
+{
+ int parsed = std::stoi(value);
+ if (parsed < 0 || parsed > 65535)
+ {
+ throw std::runtime_error(name + " must fit uint16");
+ }
+ return static_cast(parsed);
+}
+
+} // namespace
+
+int main(int argc, char** argv)
+try
+{
+ NoitomMocapPluginConfig config;
+ double rate_hz = 90.0;
+
+ for (int i = 1; i < argc; ++i)
+ {
+ std::string arg = argv[i];
+ if (arg == "--help" || arg == "-h")
+ {
+ print_usage(argv[0]);
+ return 0;
+ }
+ if (arg == "--plugin-root-id")
+ {
+ if (i + 1 >= argc)
+ {
+ throw std::runtime_error("--plugin-root-id requires a value");
+ }
+ ++i;
+ continue;
+ }
+ if (arg.find("--protocol=") == 0)
+ {
+ std::string value = arg.substr(11);
+ if (value == "tcp")
+ config.protocol = MocapProtocol::Tcp;
+ else if (value == "udp")
+ config.protocol = MocapProtocol::Udp;
+ else
+ throw std::runtime_error("--protocol must be tcp or udp");
+ }
+ else if (arg.find("--host=") == 0)
+ {
+ config.host = arg.substr(7);
+ }
+ else if (arg.find("--port=") == 0)
+ {
+ config.port = parse_u16(arg.substr(7), "--port");
+ }
+ else if (arg.find("--udp-local-port=") == 0)
+ {
+ config.udp_local_port = parse_u16(arg.substr(17), "--udp-local-port");
+ }
+ else if (arg.find("--udp-server-host=") == 0)
+ {
+ config.udp_server_host = arg.substr(18);
+ }
+ else if (arg.find("--udp-server-port=") == 0)
+ {
+ config.udp_server_port = parse_u16(arg.substr(18), "--udp-server-port");
+ }
+ else if (arg.find("--collection-id=") == 0)
+ {
+ config.collection_id = arg.substr(16);
+ }
+ else if (arg.find("--max-flatbuffer-size=") == 0)
+ {
+ config.max_flatbuffer_size = static_cast(std::stoull(arg.substr(22)));
+ }
+ else if (arg.find("--rate=") == 0)
+ {
+ rate_hz = std::stod(arg.substr(7));
+ if (rate_hz <= 0.0)
+ {
+ throw std::runtime_error("--rate must be positive");
+ }
+ }
+ else if (arg.find("--plugin-root-id=") == 0)
+ {
+ continue;
+ }
+ else
+ {
+ std::cerr << "Unknown option: " << arg << std::endl;
+ print_usage(argv[0]);
+ return 1;
+ }
+ }
+
+ std::signal(SIGINT, signal_handler);
+ std::signal(SIGTERM, signal_handler);
+
+ NoitomMocapPlugin plugin(config);
+
+ const auto frame_duration = std::chrono::nanoseconds(static_cast(1000000000.0 / rate_hz));
+ const auto program_start = std::chrono::steady_clock::now();
+ std::size_t frame_count = 0;
+
+ while (!g_stop_requested.load(std::memory_order_relaxed))
+ {
+ if (!plugin.update())
+ {
+ return 1;
+ }
+ ++frame_count;
+ std::this_thread::sleep_until(program_start + frame_duration * frame_count);
+ }
+
+ return 0;
+}
+catch (const std::exception& e)
+{
+ std::cerr << argv[0] << ": " << e.what() << std::endl;
+ return 1;
+}
+catch (...)
+{
+ std::cerr << argv[0] << ": Unknown error" << std::endl;
+ return 1;
+}
diff --git a/src/plugins/noitom_mocap/noitom_mocap_plugin.cpp b/src/plugins/noitom_mocap/noitom_mocap_plugin.cpp
new file mode 100644
index 000000000..851071f73
--- /dev/null
+++ b/src/plugins/noitom_mocap/noitom_mocap_plugin.cpp
@@ -0,0 +1,545 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+#include "noitom_mocap_plugin.hpp"
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace plugins
+{
+namespace noitom_mocap
+{
+
+namespace
+{
+
+constexpr float SDK_CENTIMETERS_TO_METERS = 0.01f;
+constexpr const char* ANSI_ORANGE = "\033[38;5;208m";
+constexpr const char* ANSI_RESET = "\033[0m";
+
+core::Point make_point(float x, float y, float z)
+{
+ // Noitom live position getters produce centimeter-scale values; publish schema positions in meters.
+ return core::Point(x * SDK_CENTIMETERS_TO_METERS, y * SDK_CENTIMETERS_TO_METERS, z * SDK_CENTIMETERS_TO_METERS);
+}
+
+core::Quaternion make_quaternion(float x, float y, float z, float w)
+{
+ return core::Quaternion(x, y, z, w);
+}
+
+std::string normalize_joint_name(std::string_view name)
+{
+ std::string result;
+ result.reserve(name.size());
+ for (char c : name)
+ {
+ if (c == ' ' || c == '_' || c == '-')
+ {
+ continue;
+ }
+ result.push_back(static_cast(std::tolower(static_cast(c))));
+ }
+ return result;
+}
+
+std::optional map_noitom_joint_name(std::string_view name)
+{
+ const std::string normalized = normalize_joint_name(name);
+ if (normalized == "hips" || normalized == "hip" || normalized == "pelvis")
+ return core::BodyJointPN_PELVIS;
+ if (normalized == "leftupleg" || normalized == "lefthip")
+ return core::BodyJointPN_LEFT_HIP;
+ if (normalized == "rightupleg" || normalized == "righthip")
+ return core::BodyJointPN_RIGHT_HIP;
+ if (normalized == "spine")
+ return core::BodyJointPN_SPINE1;
+ if (normalized == "spine1")
+ return core::BodyJointPN_SPINE2;
+ if (normalized == "spine2" || normalized == "chest")
+ return core::BodyJointPN_SPINE3;
+ if (normalized == "leftleg" || normalized == "leftknee")
+ return core::BodyJointPN_LEFT_KNEE;
+ if (normalized == "rightleg" || normalized == "rightknee")
+ return core::BodyJointPN_RIGHT_KNEE;
+ if (normalized == "leftfoot" || normalized == "leftankle")
+ return core::BodyJointPN_LEFT_ANKLE;
+ if (normalized == "rightfoot" || normalized == "rightankle")
+ return core::BodyJointPN_RIGHT_ANKLE;
+ if (normalized == "lefttoebase" || normalized == "lefttoe")
+ return core::BodyJointPN_LEFT_FOOT;
+ if (normalized == "righttoebase" || normalized == "righttoe")
+ return core::BodyJointPN_RIGHT_FOOT;
+ if (normalized == "neck")
+ return core::BodyJointPN_NECK;
+ if (normalized == "head")
+ return core::BodyJointPN_HEAD;
+ if (normalized == "leftshoulder" || normalized == "leftcollar")
+ return core::BodyJointPN_LEFT_COLLAR;
+ if (normalized == "rightshoulder" || normalized == "rightcollar")
+ return core::BodyJointPN_RIGHT_COLLAR;
+ if (normalized == "leftarm" || normalized == "leftupperarm")
+ return core::BodyJointPN_LEFT_SHOULDER;
+ if (normalized == "rightarm" || normalized == "rightupperarm")
+ return core::BodyJointPN_RIGHT_SHOULDER;
+ if (normalized == "leftforearm" || normalized == "leftelbow")
+ return core::BodyJointPN_LEFT_ELBOW;
+ if (normalized == "rightforearm" || normalized == "rightelbow")
+ return core::BodyJointPN_RIGHT_ELBOW;
+ if (normalized == "lefthand" || normalized == "leftwrist")
+ return core::BodyJointPN_LEFT_WRIST;
+ if (normalized == "righthand" || normalized == "rightwrist")
+ return core::BodyJointPN_RIGHT_WRIST;
+ if (normalized == "lefthandend" || normalized == "lefthandtip")
+ return core::BodyJointPN_LEFT_HAND;
+ if (normalized == "righthandend" || normalized == "righthandtip")
+ return core::BodyJointPN_RIGHT_HAND;
+ return std::nullopt;
+}
+
+core::BodyJointPose make_invalid_body_joint_pose()
+{
+ return core::BodyJointPose(core::Pose(core::Point(), core::Quaternion()), false);
+}
+
+int64_t ptp_to_nanoseconds(uint32_t second, uint32_t nanosecond)
+{
+ if (second == 0 && nanosecond == 0)
+ {
+ return 0;
+ }
+ return static_cast(second) * 1000000000LL + static_cast(nanosecond);
+}
+
+std::string error_name(MocapApi::EMCPError err)
+{
+ switch (err)
+ {
+ case MocapApi::Error_None:
+ return "None";
+ case MocapApi::Error_MoreEvent:
+ return "MoreEvent";
+ case MocapApi::Error_ServerNotReady:
+ return "ServerNotReady";
+ case MocapApi::Error_ClientNotReady:
+ return "ClientNotReady";
+ case MocapApi::Error_TCP:
+ return "TCP";
+ case MocapApi::Error_UDP:
+ return "UDP";
+ default:
+ return "code_" + std::to_string(static_cast(err));
+ }
+}
+
+std::string error_string(MocapApi::EMCPError err)
+{
+ return "MocapApi " + error_name(err) + " (" + std::to_string(static_cast(err)) + ")";
+}
+
+void check_mocap(MocapApi::EMCPError err, const std::string& context)
+{
+ if (err != MocapApi::Error_None)
+ {
+ throw std::runtime_error(context + ": " + error_string(err));
+ }
+}
+
+void warn_optional_ptp_missing_once()
+{
+ static bool warned = false;
+ if (warned)
+ {
+ return;
+ }
+ warned = true;
+ std::cerr << ANSI_ORANGE
+ << "NoitomMocapPlugin: warning: avatar PTP timestamp is unavailable; publishing frames with ptp=0"
+ << ANSI_RESET << std::endl;
+}
+
+} // namespace
+
+NoitomMocapPlugin::NoitomMocapPlugin(NoitomMocapPluginConfig config)
+ : config_(std::move(config)),
+ session_(std::make_shared("NoitomMocapPlugin", core::SchemaPusher::get_required_extensions()))
+{
+ initialize_mocap();
+}
+
+NoitomMocapPlugin::~NoitomMocapPlugin()
+{
+ close_mocap();
+}
+
+template
+InterfaceT* NoitomMocapPlugin::get_interface(const char* version)
+{
+ void* iface = nullptr;
+ check_mocap(
+ MocapApi::MCPGetGenericInterface(version, &iface), std::string("MCPGetGenericInterface(") + version + ")");
+ if (!iface)
+ {
+ throw std::runtime_error(std::string("MCPGetGenericInterface(") + version + ") returned null");
+ }
+ return static_cast(iface);
+}
+
+void NoitomMocapPlugin::initialize_mocap()
+{
+ settings_api_ = get_interface(MocapApi::IMCPSettings_Version);
+ application_api_ = get_interface(MocapApi::IMCPApplication_Version);
+ avatar_api_ = get_interface(MocapApi::IMCPAvatar_Version);
+ joint_api_ = get_interface(MocapApi::IMCPJoint_Version);
+ render_settings_api_ = get_interface(MocapApi::IMCPRenderSettings_Version);
+
+ check_mocap(settings_api_->CreateSettings(&settings_handle_), "CreateSettings");
+ if (config_.protocol == MocapProtocol::Tcp)
+ {
+ check_mocap(
+ settings_api_->SetSettingsTCP(config_.host.c_str(), config_.port, settings_handle_), "SetSettingsTCP");
+ }
+ else
+ {
+ check_mocap(settings_api_->SetSettingsUDP(config_.udp_local_port, settings_handle_), "SetSettingsUDP");
+ if (!config_.udp_server_host.empty())
+ {
+ check_mocap(settings_api_->SetSettingsUDPServer(
+ config_.udp_server_host.c_str(), config_.udp_server_port, settings_handle_),
+ "SetSettingsUDPServer");
+ }
+ }
+ check_mocap(
+ settings_api_->SetSettingsBvhRotation(MocapApi::BvhRotation_XYZ, settings_handle_), "SetSettingsBvhRotation");
+ check_mocap(render_settings_api_->CreateRenderSettings(&render_settings_handle_), "CreateRenderSettings");
+ check_mocap(
+ render_settings_api_->SetUnit(MocapApi::Unit_Centimeter, render_settings_handle_), "SetUnit(Unit_Centimeter)");
+
+ MocapApi::EMCPUnit unit = MocapApi::Uint_Meter;
+ check_mocap(render_settings_api_->GetUnit(&unit, render_settings_handle_), "GetUnit");
+ if (unit != MocapApi::Unit_Centimeter)
+ {
+ throw std::runtime_error("NoitomMocapPlugin: failed to configure SDK position units to centimeters");
+ }
+
+ check_mocap(application_api_->CreateApplication(&application_handle_), "CreateApplication");
+ check_mocap(
+ application_api_->SetApplicationSettings(settings_handle_, application_handle_), "SetApplicationSettings");
+ check_mocap(application_api_->SetApplicationRenderSettings(render_settings_handle_, application_handle_),
+ "SetApplicationRenderSettings");
+ check_mocap(application_api_->OpenApplication(application_handle_), "OpenApplication");
+ application_open_ = true;
+
+ bool cache_events_enabled = false;
+ const auto cache_err = application_api_->EnableApplicationCacheEvents(application_handle_);
+ if (cache_err == MocapApi::Error_None)
+ {
+ check_mocap(application_api_->ApplicationCacheEventsIsEnabled(&cache_events_enabled, application_handle_),
+ "ApplicationCacheEventsIsEnabled");
+ }
+ else if (cache_err == MocapApi::Error_NotSupported)
+ {
+ std::cerr << ANSI_ORANGE
+ << "NoitomMocapPlugin: warning: SDK application event cache is not supported; "
+ "continuing with polling"
+ << ANSI_RESET << std::endl;
+ }
+ else
+ {
+ check_mocap(cache_err, "EnableApplicationCacheEvents");
+ }
+
+ std::cout << "NoitomMocapPlugin: connected via " << (config_.protocol == MocapProtocol::Tcp ? "TCP" : "UDP")
+ << ", collection=" << config_.collection_id << ", sdk_units=centimeters, output_units=meters, event_cache="
+ << (cache_events_enabled ? "enabled" : "disabled") << std::endl;
+}
+
+void NoitomMocapPlugin::close_mocap()
+{
+ if (application_api_ && application_handle_)
+ {
+ if (application_open_)
+ {
+ application_api_->CloseApplication(application_handle_);
+ application_open_ = false;
+ }
+ application_api_->DestroyApplication(application_handle_);
+ application_handle_ = 0;
+ }
+
+ if (settings_api_ && settings_handle_)
+ {
+ settings_api_->DestroySettings(settings_handle_);
+ settings_handle_ = 0;
+ }
+
+ if (render_settings_api_ && render_settings_handle_)
+ {
+ render_settings_api_->DestroyRenderSettings(render_settings_handle_);
+ render_settings_handle_ = 0;
+ }
+}
+
+std::vector NoitomMocapPlugin::poll_events()
+{
+ uint32_t event_count = 0;
+ MocapApi::EMCPError err = application_api_->PollApplicationNextEvent(nullptr, &event_count, application_handle_);
+ if (err != MocapApi::Error_None && err != MocapApi::Error_MoreEvent)
+ {
+ throw std::runtime_error("PollApplicationNextEvent(count): " + error_string(err));
+ }
+ if (event_count == 0)
+ {
+ return {};
+ }
+
+ std::vector events(event_count);
+ for (auto& event : events)
+ {
+ event.size = sizeof(MocapApi::MCPEvent_t);
+ }
+
+ err = application_api_->PollApplicationNextEvent(events.data(), &event_count, application_handle_);
+ if (err != MocapApi::Error_None && err != MocapApi::Error_MoreEvent)
+ {
+ throw std::runtime_error("PollApplicationNextEvent(events): " + error_string(err));
+ }
+ events.resize(event_count);
+ return events;
+}
+
+std::vector NoitomMocapPlugin::poll_avatars()
+{
+ uint32_t avatar_count = 0;
+ auto err = application_api_->GetApplicationAvatars(nullptr, &avatar_count, application_handle_);
+ if (err != MocapApi::Error_None)
+ {
+ throw std::runtime_error("GetApplicationAvatars(count): " + error_string(err));
+ }
+ if (avatar_count == 0)
+ {
+ if (!warned_no_avatars_)
+ {
+ warned_no_avatars_ = true;
+ std::cerr << ANSI_ORANGE
+ << "NoitomMocapPlugin: warning: SDK reports zero avatars; waiting for HDS avatar data"
+ << ANSI_RESET << std::endl;
+ }
+ return {};
+ }
+
+ warned_no_avatars_ = false;
+ std::vector avatars(avatar_count);
+ check_mocap(application_api_->GetApplicationAvatars(avatars.data(), &avatar_count, application_handle_),
+ "GetApplicationAvatars");
+ avatars.resize(avatar_count);
+ return avatars;
+}
+
+bool NoitomMocapPlugin::handle_avatar(MocapApi::MCPAvatarHandle_t avatar_handle)
+{
+ uint32_t avatar_index = 0;
+ uint32_t posture_index = 0;
+ uint32_t ptp_second = 0;
+ uint32_t ptp_nanosecond = 0;
+ check_mocap(avatar_api_->GetAvatarIndex(&avatar_index, avatar_handle), "GetAvatarIndex");
+ check_mocap(avatar_api_->GetAvatarPostureIndex(&posture_index, avatar_handle), "GetAvatarPostureIndex");
+ const MocapApi::EMCPError ptp_err = avatar_api_->GetAvatarPosturePtpTime(&ptp_second, &ptp_nanosecond, avatar_handle);
+ if (ptp_err == MocapApi::Error_NoneMessage)
+ {
+ warn_optional_ptp_missing_once();
+ }
+ else
+ {
+ check_mocap(ptp_err, "GetAvatarPosturePtpTime");
+ }
+ latest_sample_time_raw_device_clock_ns_ = ptp_to_nanoseconds(ptp_second, ptp_nanosecond);
+
+ uint32_t joint_count = 0;
+ auto err = avatar_api_->GetAvatarJoints(nullptr, &joint_count, avatar_handle);
+ if (err != MocapApi::Error_None || joint_count == 0)
+ {
+ frame_.joints.reset();
+ return false;
+ }
+
+ std::vector joints(joint_count);
+ check_mocap(avatar_api_->GetAvatarJoints(joints.data(), &joint_count, avatar_handle), "GetAvatarJoints");
+
+ frame_.joints = std::make_shared();
+ frame_.all_joint_poses_tracked = false;
+ std::array seen{};
+ for (uint8_t i = 0; i < static_cast(core::BodyJointPN_NUM_JOINTS); ++i)
+ {
+ frame_.joints->mutable_joints()->Mutate(i, make_invalid_body_joint_pose());
+ }
+
+ for (auto handle : joints)
+ {
+ const char* name = nullptr;
+ float gx = 0.0f;
+ float gy = 0.0f;
+ float gz = 0.0f;
+ float gqx = 0.0f;
+ float gqy = 0.0f;
+ float gqz = 0.0f;
+ float gqw = 1.0f;
+
+ bool valid = true;
+ valid &= (joint_api_->GetJointName(&name, handle) == MocapApi::Error_None);
+ valid &= (joint_api_->GetJointGlobalPosition(&gx, &gy, &gz, handle) == MocapApi::Error_None);
+ valid &= (joint_api_->GetJointGlobalRotation(&gqx, &gqy, &gqz, &gqw, handle) == MocapApi::Error_None);
+
+ const auto slot = map_noitom_joint_name(name ? name : "");
+ if (!slot)
+ {
+ continue;
+ }
+
+ const auto index = static_cast(*slot);
+ core::Pose pose(make_point(gx, gy, gz), make_quaternion(gqx, gqy, gqz, gqw));
+ core::BodyJointPose joint_pose(pose, valid);
+ frame_.joints->mutable_joints()->Mutate(index, joint_pose);
+ seen[index] = valid;
+
+ if (*slot == core::BodyJointPN_LEFT_WRIST)
+ {
+ frame_.joints->mutable_joints()->Mutate(core::BodyJointPN_LEFT_HAND, joint_pose);
+ seen[core::BodyJointPN_LEFT_HAND] = valid;
+ }
+ else if (*slot == core::BodyJointPN_RIGHT_WRIST)
+ {
+ frame_.joints->mutable_joints()->Mutate(core::BodyJointPN_RIGHT_HAND, joint_pose);
+ seen[core::BodyJointPN_RIGHT_HAND] = valid;
+ }
+ else if (*slot == core::BodyJointPN_LEFT_ANKLE)
+ {
+ frame_.joints->mutable_joints()->Mutate(core::BodyJointPN_LEFT_FOOT, joint_pose);
+ seen[core::BodyJointPN_LEFT_FOOT] = valid;
+ }
+ else if (*slot == core::BodyJointPN_RIGHT_ANKLE)
+ {
+ frame_.joints->mutable_joints()->Mutate(core::BodyJointPN_RIGHT_FOOT, joint_pose);
+ seen[core::BodyJointPN_RIGHT_FOOT] = valid;
+ }
+ }
+
+ frame_.all_joint_poses_tracked = std::all_of(seen.begin(), seen.end(), [](bool value) { return value; });
+ std::cout << "NoitomMocapPlugin: converted avatar=" << avatar_index << " posture=" << posture_index
+ << " valid_full_body_joints=" << std::count(seen.begin(), seen.end(), true) << "/"
+ << static_cast(core::BodyJointPN_NUM_JOINTS) << std::endl;
+ return true;
+}
+
+bool NoitomMocapPlugin::update()
+{
+ try
+ {
+ auto events = poll_events();
+ bool should_push = false;
+
+ for (const auto& event : events)
+ {
+ switch (event.eventType)
+ {
+ case MocapApi::MCPEvent_AvatarUpdated:
+ should_push = handle_avatar(event.eventData.motionData.avatarHandle) || should_push;
+ break;
+ case MocapApi::MCPEvent_Error:
+ {
+ const auto sdk_err = event.eventData.systemError.error;
+ std::cerr << ANSI_ORANGE << "NoitomMocapPlugin: warning: SDK error event " << error_string(sdk_err);
+ if (sdk_err == MocapApi::Error_ServerNotReady)
+ {
+ std::cerr << " — Hybrid Data Server is not streaming avatar data yet. "
+ "On Windows: start Axis Studio calibration, then enable HDS TCP "
+ "broadcast on this port";
+ }
+ std::cerr << ANSI_RESET << std::endl;
+ break;
+ }
+ default:
+ break;
+ }
+ }
+
+ if (!should_push)
+ {
+ for (auto avatar_handle : poll_avatars())
+ {
+ should_push = handle_avatar(avatar_handle) || should_push;
+ }
+ }
+
+ if (should_push)
+ {
+ const int64_t sample_time_ns = core::os_monotonic_now_ns();
+ const int64_t raw_device_time_ns =
+ latest_sample_time_raw_device_clock_ns_ == 0 ? sample_time_ns : latest_sample_time_raw_device_clock_ns_;
+ push_frame(sample_time_ns, raw_device_time_ns);
+ }
+ return true;
+ }
+ catch (const std::exception& e)
+ {
+ std::cerr << "NoitomMocapPlugin: fatal: " << e.what() << " (check HDS TCP broadcast and: nc -zv "
+ << config_.host << " " << config_.port << ")" << std::endl;
+ return false;
+ }
+ catch (...)
+ {
+ std::cerr << "NoitomMocapPlugin: fatal: Noitom SDK connection lost (software caused connection abort). "
+ << "Ensure Windows HDS is broadcasting TCP on " << config_.host << ":" << config_.port
+ << " and verify with: nc -zv " << config_.host << " " << config_.port << std::endl;
+ return false;
+ }
+}
+
+void NoitomMocapPlugin::ensure_pusher(size_t flatbuffer_size)
+{
+ if (flatbuffer_size == 0 || flatbuffer_size > config_.max_flatbuffer_size)
+ {
+ throw std::runtime_error("NoitomMocapPlugin: serialized frame size " + std::to_string(flatbuffer_size) +
+ " exceeds max_flatbuffer_size " + std::to_string(config_.max_flatbuffer_size));
+ }
+
+ if (pusher_)
+ {
+ return;
+ }
+
+ // Keep the OpenXR tensor collection stable. SchemaPusher pads smaller samples
+ // to max_flatbuffer_size before publishing.
+ pusher_ = std::make_unique(
+ session_->get_handles(), core::SchemaPusherConfig{ .collection_id = config_.collection_id,
+ .max_flatbuffer_size = config_.max_flatbuffer_size,
+ .tensor_identifier = "full_body_pn",
+ .localized_name = "Noitom Full Body PN",
+ .app_name = "NoitomMocapPlugin" });
+ std::cout << "NoitomMocapPlugin: push tensor sample size set to " << config_.max_flatbuffer_size << " bytes"
+ << std::endl;
+}
+
+void NoitomMocapPlugin::push_frame(int64_t sample_time_local_common_clock_ns, int64_t sample_time_raw_device_clock_ns)
+{
+ flatbuffers::FlatBufferBuilder builder(config_.max_flatbuffer_size);
+ auto offset = core::FullBodyPosePN::Pack(builder, &frame_);
+ builder.Finish(offset);
+ ensure_pusher(builder.GetSize());
+ pusher_->push_buffer(builder.GetBufferPointer(), builder.GetSize(), sample_time_local_common_clock_ns,
+ sample_time_raw_device_clock_ns);
+}
+
+} // namespace noitom_mocap
+} // namespace plugins
diff --git a/src/plugins/noitom_mocap/noitom_mocap_plugin.hpp b/src/plugins/noitom_mocap/noitom_mocap_plugin.hpp
new file mode 100644
index 000000000..55718f804
--- /dev/null
+++ b/src/plugins/noitom_mocap/noitom_mocap_plugin.hpp
@@ -0,0 +1,88 @@
+// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+// SPDX-License-Identifier: Apache-2.0
+
+#pragma once
+
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+
+namespace core
+{
+class OpenXRSession;
+}
+
+namespace plugins
+{
+namespace noitom_mocap
+{
+
+static constexpr size_t DEFAULT_NOITOM_MOCAP_MAX_FLATBUFFER_SIZE = 16 * 1024;
+
+enum class MocapProtocol
+{
+ Tcp,
+ Udp,
+};
+
+struct NoitomMocapPluginConfig
+{
+ MocapProtocol protocol = MocapProtocol::Tcp;
+ std::string host = "127.0.0.1";
+ uint16_t port = 8001;
+ uint16_t udp_local_port = 8002;
+ std::string udp_server_host;
+ uint16_t udp_server_port = 8001;
+ std::string collection_id = "noitom_mocap";
+ size_t max_flatbuffer_size = DEFAULT_NOITOM_MOCAP_MAX_FLATBUFFER_SIZE;
+};
+
+class NoitomMocapPlugin
+{
+public:
+ explicit NoitomMocapPlugin(NoitomMocapPluginConfig config);
+ ~NoitomMocapPlugin();
+
+ // Returns false when the Noitom SDK connection is lost (caller should exit).
+ bool update();
+
+private:
+ template
+ InterfaceT* get_interface(const char* version);
+
+ void initialize_mocap();
+ void close_mocap();
+ std::vector poll_events();
+ std::vector poll_avatars();
+ bool handle_avatar(MocapApi::MCPAvatarHandle_t avatar_handle);
+ void ensure_pusher(size_t flatbuffer_size);
+ void push_frame(int64_t sample_time_local_common_clock_ns, int64_t sample_time_raw_device_clock_ns);
+
+ NoitomMocapPluginConfig config_;
+ std::shared_ptr session_;
+ std::unique_ptr pusher_;
+
+ MocapApi::IMCPSettings* settings_api_ = nullptr;
+ MocapApi::IMCPApplication* application_api_ = nullptr;
+ MocapApi::IMCPAvatar* avatar_api_ = nullptr;
+ MocapApi::IMCPJoint* joint_api_ = nullptr;
+ MocapApi::IMCPRenderSettings* render_settings_api_ = nullptr;
+
+ MocapApi::MCPSettingsHandle_t settings_handle_ = 0;
+ MocapApi::MCPRenderSettingsHandle_t render_settings_handle_ = 0;
+ MocapApi::MCPApplicationHandle_t application_handle_ = 0;
+ bool application_open_ = false;
+ bool warned_no_avatars_ = false;
+
+ core::FullBodyPosePNT frame_;
+ int64_t latest_sample_time_raw_device_clock_ns_ = 0;
+};
+
+} // namespace noitom_mocap
+} // namespace plugins
diff --git a/src/plugins/noitom_mocap/plugin.yaml b/src/plugins/noitom_mocap/plugin.yaml
new file mode 100644
index 000000000..d353c4333
--- /dev/null
+++ b/src/plugins/noitom_mocap/plugin.yaml
@@ -0,0 +1,11 @@
+# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+name: noitom_mocap
+description: "Noitom mocap stream via Hybrid Data Server and OpenXR tensor data"
+command: "./noitom_mocap_plugin"
+version: "0.1.0"
+devices:
+ - path: "/mocap/noitom"
+ type: "noitom_mocap"
+ description: "Noitom avatar joints and PWR trackers"
diff --git a/src/plugins/noitom_mocap/third_party/.gitignore b/src/plugins/noitom_mocap/third_party/.gitignore
new file mode 100644
index 000000000..2b266c48b
--- /dev/null
+++ b/src/plugins/noitom_mocap/third_party/.gitignore
@@ -0,0 +1,6 @@
+# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+# The Noitom MocapApi SDK
+!mocap_api/lib/arm64/libMocapApi.so
+!mocap_api/lib/x86_64/libMocapApi.so
diff --git a/src/plugins/noitom_mocap/third_party/mocap_api/__init__.py b/src/plugins/noitom_mocap/third_party/mocap_api/__init__.py
new file mode 100644
index 000000000..76c7344af
--- /dev/null
+++ b/src/plugins/noitom_mocap/third_party/mocap_api/__init__.py
@@ -0,0 +1,125 @@
+# mocap/__init__.py
+
+# Import all objects that need to be exported from mocap_api
+from .mocap_api import (
+ MCPError,
+ MCPJointTag,
+ MCPRigidBodyHandle,
+ MCPRigidBody,
+ MCPSensorModuleHandle,
+ MCPSensorModule,
+ MCPBodyPartHandle,
+ MCPBodyPart,
+ MCPJointHandle,
+ MCPJoint,
+ MCPAvatarHandle,
+ MCPAvatar,
+ MCPCalibrateMotionProgressStep,
+ MCPCalibrateMotionProgressHandle,
+ MCPCalibrateMotionProgress,
+ MCPCommandProgress,
+ MCPCommandHandle,
+ MCPCommand,
+ MCPEventDataReserved,
+ MCPEventCommandRespond,
+ MCPReplay,
+ MCPPWRHandle,
+ MCPPWR,
+ MCPMarkerHandle,
+ MCPAliceBusHandle,
+ MCPAliceHub,
+ MCPSensorModuleHandle,
+ MCPSystemHandle,
+ MCPSystem,
+ MCPEventNotify,
+ MCPEventNotifyData,
+ MCPTrackerHandle,
+ MCPEventData,
+ MCPEventType,
+ MCPEvent,
+ MCPBvhRotation,
+ MCPBvhData,
+ MCPBvhDisplacement,
+ MCPTracker,
+ MCPMarker,
+ MCPSettingsHandle,
+ MCPSettings,
+ MCPUpVector,
+ MCPFrontVector,
+ MCPCoordSystem,
+ MCPRotatingDirection,
+ MCPPreDefinedRenderSettings,
+ MCPUnit,
+ MCPRenderSettingsHandle,
+ MCPRenderSettings,
+ MCPRobotJointType,
+ MCPRobotHandle,
+ MCPRobot,
+ EMCPCommand,
+ MCPApplicationHandle,
+ MCPTrackerHandle,
+ MCPEventHandleProc,
+ MCPApplication
+)
+
+# Define package-level __all__ to control what's exported when using 'from mocap import *'
+__all__ = [
+ 'MCPError',
+ 'MCPJointTag',
+ 'MCPRigidBodyHandle',
+ 'MCPRigidBody',
+ 'MCPSensorModuleHandle',
+ 'MCPSensorModule',
+ 'MCPBodyPartHandle',
+ 'MCPBodyPart',
+ 'MCPJointHandle',
+ 'MCPJoint',
+ 'MCPAvatarHandle',
+ 'MCPAvatar',
+ 'MCPCalibrateMotionProgressStep',
+ 'MCPCalibrateMotionProgressHandle',
+ 'MCPCalibrateMotionProgress',
+ 'MCPCommandProgress',
+ 'MCPCommandHandle',
+ 'MCPCommand',
+ 'MCPEventDataReserved',
+ 'MCPEventCommandRespond',
+ 'MCPReplay',
+ 'MCPPWRHandle',
+ 'MCPPWR',
+ 'MCPMarkerHandle',
+ 'MCPAliceBusHandle',
+ 'MCPAliceHub',
+ 'MCPSensorModuleHandle',
+ 'MCPSystemHandle',
+ 'MCPSystem',
+ 'MCPEventNotify',
+ 'MCPEventNotifyData',
+ 'MCPTrackerHandle',
+ 'MCPEventData',
+ 'MCPEventType',
+ 'MCPEvent',
+ 'MCPBvhRotation',
+ 'MCPBvhData',
+ 'MCPBvhDisplacement',
+ 'MCPTracker',
+ 'MCPMarker',
+ 'MCPSettingsHandle',
+ 'MCPSettings',
+ 'MCPUpVector',
+ 'MCPFrontVector',
+ 'MCPCoordSystem',
+ 'MCPRotatingDirection',
+ 'MCPPreDefinedRenderSettings',
+ 'MCPUnit',
+ 'MCPRenderSettingsHandle',
+ 'MCPRenderSettings',
+ 'MCPRobotJointType',
+ 'MCPRobotHandle',
+ 'MCPRobot',
+ 'EMCPCommand',
+ 'MCPApplicationHandle',
+ 'MCPTrackerHandle',
+ 'MCPEventHandleProc',
+ 'MCPApplication'
+]
\ No newline at end of file
diff --git a/src/plugins/noitom_mocap/third_party/mocap_api/include/MocapApi/MocapApi.h b/src/plugins/noitom_mocap/third_party/mocap_api/include/MocapApi/MocapApi.h
new file mode 100644
index 000000000..2560fae81
--- /dev/null
+++ b/src/plugins/noitom_mocap/third_party/mocap_api/include/MocapApi/MocapApi.h
@@ -0,0 +1,641 @@
+// clang-format off
+#ifndef _NOITOM_MOCAPAPI_H
+#define _NOITOM_MOCAPAPI_H
+
+#include
+#if defined(_WIN32)
+#define MCP_CALLTYPE __cdecl
+#ifdef MCP_API_EXPORT
+#define MCP_INTERFACE extern "C" __declspec( dllexport )
+#elif (defined MCP_API_STATIC)
+#define MCP_INTERFACE extern "C"
+#else
+#define MCP_INTERFACE extern "C" __declspec( dllimport )
+#endif
+#elif defined(__GNUC__) || defined(COMPILER_GCC) || defined(__APPLE__)
+#define MCP_CALLTYPE
+#ifdef MCP_API_EXPORT
+#define MCP_INTERFACE extern "C" __attribute__((visibility("default")))
+#else
+#define MCP_INTERFACE extern "C"
+#endif
+#else
+#error "Unsupported Platform."
+#endif
+
+#define MOCAP_API_VERSION_MAJOR 0
+#define MOCAP_API_VERSION_MINOR 0
+#define MOCAP_API_VERSION_BUILD 73
+#define MOCAP_API_VERSION_REVISION 19a54f85
+
+namespace MocapApi
+{
+ enum EMCPError
+ {
+ Error_None=0,
+ Error_MoreEvent=1,
+ Error_InsufficientBuffer=2,
+ Error_InvalidObject=3,
+ Error_InvalidHandle=4,
+ Error_InvalidParameter=5,
+ Error_NotSupported=6,
+ Error_IgnoreUDPSetting=7,
+ Error_IgnoreTCPSetting=8,
+ Error_IgnoreBvhSetting=9,
+ Error_JointNotFound=10,
+ Error_WithoutTransformation=11,
+ Error_NoneMessage=12,
+ Error_NoneParent=13,
+ Error_NoneChild=14,
+ Error_AddressInUse=15,
+ Error_ServerNotReady=16,
+ Error_ClientNotReady=17,
+ Error_IncompleteCommand=18,
+ Error_UDP=19,
+ Error_TCP=20,
+ Error_QueuedCommandFaild=21,
+ Error_InterfaceIncompatible=22
+ };
+ enum EMCPJointTag
+ {
+ JointTag_Invalid=-1,
+ JointTag_Hips=0,
+ JointTag_RightUpLeg=1,
+ JointTag_RightLeg=2,
+ JointTag_RightFoot=3,
+ JointTag_LeftUpLeg=4,
+ JointTag_LeftLeg=5,
+ JointTag_LeftFoot=6,
+ JointTag_Spine=7,
+ JointTag_Spine1=8,
+ JointTag_Spine2=9,
+ JointTag_Neck=10,
+ JointTag_Neck1=11,
+ JointTag_Head=12,
+ JointTag_RightShoulder=13,
+ JointTag_RightArm=14,
+ JointTag_RightForeArm=15,
+ JointTag_RightHand=16,
+ JointTag_RightHandThumb1=17,
+ JointTag_RightHandThumb2=18,
+ JointTag_RightHandThumb3=19,
+ JointTag_RightInHandIndex=20,
+ JointTag_RightHandIndex1=21,
+ JointTag_RightHandIndex2=22,
+ JointTag_RightHandIndex3=23,
+ JointTag_RightInHandMiddle=24,
+ JointTag_RightHandMiddle1=25,
+ JointTag_RightHandMiddle2=26,
+ JointTag_RightHandMiddle3=27,
+ JointTag_RightInHandRing=28,
+ JointTag_RightHandRing1=29,
+ JointTag_RightHandRing2=30,
+ JointTag_RightHandRing3=31,
+ JointTag_RightInHandPinky=32,
+ JointTag_RightHandPinky1=33,
+ JointTag_RightHandPinky2=34,
+ JointTag_RightHandPinky3=35,
+ JointTag_LeftShoulder=36,
+ JointTag_LeftArm=37,
+ JointTag_LeftForeArm=38,
+ JointTag_LeftHand=39,
+ JointTag_LeftHandThumb1=40,
+ JointTag_LeftHandThumb2=41,
+ JointTag_LeftHandThumb3=42,
+ JointTag_LeftInHandIndex=43,
+ JointTag_LeftHandIndex1=44,
+ JointTag_LeftHandIndex2=45,
+ JointTag_LeftHandIndex3=46,
+ JointTag_LeftInHandMiddle=47,
+ JointTag_LeftHandMiddle1=48,
+ JointTag_LeftHandMiddle2=49,
+ JointTag_LeftHandMiddle3=50,
+ JointTag_LeftInHandRing=51,
+ JointTag_LeftHandRing1=52,
+ JointTag_LeftHandRing2=53,
+ JointTag_LeftHandRing3=54,
+ JointTag_LeftInHandPinky=55,
+ JointTag_LeftHandPinky1=56,
+ JointTag_LeftHandPinky2=57,
+ JointTag_LeftHandPinky3=58,
+ JointTag_Spine3=59,
+ JointTag_JointsCount=60
+ };
+ typedef uint64_t MCPRigidBodyHandle_t;
+ class IMCPRigidBody
+ {
+ public:
+ virtual EMCPError GetRigidBodyRotation(float * x, float * y, float * z, float * w, MCPRigidBodyHandle_t ulRigidBodyHandle) = 0;
+ virtual EMCPError GetRigidBodyPosition(float * x, float * y, float * z, MCPRigidBodyHandle_t ulRigidBodyHandle) = 0;
+ virtual EMCPError GetRigidBodyStatus(int * status, MCPRigidBodyHandle_t ulRigidBodyHandle) = 0;
+ virtual EMCPError GetRigidBodyId(int * id, MCPRigidBodyHandle_t ulRigidBodyHandle) = 0;
+ virtual EMCPError GetRigidBodyJointTag(EMCPJointTag * jointTag_, MCPRigidBodyHandle_t ulRigidBodyHandle) = 0;
+ virtual EMCPError GetRigidBodyAxisAngle(float * x, float * y, float * z, float * angle, MCPRigidBodyHandle_t ulRigidBodyHandle) = 0;
+ virtual EMCPError GetRigidBodyType(uint32_t * type, MCPRigidBodyHandle_t ulRigidBodyHandle) = 0;
+ virtual EMCPError GetRigidBodyName(const char ** name, MCPRigidBodyHandle_t ulRigidBodyHandle) = 0;
+ };
+ static const char * IMCPRigidBody_Version = "IMCPRigidBody_002";
+ typedef uint64_t MCPTrackerHandle_t;
+ class IMCPTracker
+ {
+ public:
+ virtual EMCPError SendMessageData(const char * message, int len, MCPTrackerHandle_t ulTrackerHandle) = 0;
+ virtual EMCPError GetTrackerRotation(float * x, float * y, float * z, float * w, const char * deviceName, MCPTrackerHandle_t ulTrackerHandle) = 0;
+ virtual EMCPError GetTrackerPosition(float * x, float * y, float * z, const char * deviceName, MCPTrackerHandle_t ulTrackerHandle) = 0;
+ virtual EMCPError GetTrackerEulerAng(float * x, float * y, float * z, const char * deviceName, MCPTrackerHandle_t ulTrackerHandle) = 0;
+ virtual EMCPError GetDeviceCount(int * devCount, MCPTrackerHandle_t ulTrackerHandle) = 0;
+ virtual EMCPError GetDeviceName(int serialNum, const char ** name, MCPTrackerHandle_t ulTrackerHandle) = 0;
+ };
+ static const char * IMCPTracker_Version = "IMCPTracker_001";
+ enum EMCPMagEnv
+ {
+ kMagEnv_Unknown=-1,
+ kMagEnv_Worst=0,
+ kMagEnv_Worse=1,
+ kMagEnv_Bad=2,
+ kMagEnv_Good=3
+ };
+ typedef uint64_t MCPSensorModuleHandle_t;
+ class IMCPSensorModule
+ {
+ public:
+ virtual EMCPError GetSensorModulePosture(float * x, float * y, float * z, float * w, MCPSensorModuleHandle_t sensorModuleHandle) = 0;
+ virtual EMCPError GetSensorModuleAngularVelocity(float * x, float * y, float * z, MCPSensorModuleHandle_t sensorModuleHandle) = 0;
+ virtual EMCPError GetSensorModuleAcceleratedVelocity(float * x, float * y, float * z, MCPSensorModuleHandle_t sensorModuleHandle) = 0;
+ virtual EMCPError GetSensorModuleId(uint32_t * id, MCPSensorModuleHandle_t sensorModuleHandle) = 0;
+ virtual EMCPError GetSensorModuleCompassValue(float * x, float * y, float * z, MCPSensorModuleHandle_t sensorModuleHandle) = 0;
+ virtual EMCPError GetSensorModuleTemperature(float * temperature, MCPSensorModuleHandle_t sensorModuleHandle) = 0;
+ virtual EMCPError GetSensorModulePostureTimeCode(uint32_t * hour, uint32_t * minute, uint32_t * second, uint32_t * frame, uint32_t * rate, MCPSensorModuleHandle_t sensorModuleHandle) = 0;
+ virtual EMCPError GetSensorModulePostureTime(uint32_t * hour, uint32_t * minute, uint32_t * second, uint32_t * millisecond, MCPSensorModuleHandle_t sensorModuleHandle) = 0;
+ virtual EMCPError GetSensorModuleBoundBone(EMCPJointTag * bone, MCPSensorModuleHandle_t sensorModuleHandle) = 0;
+ virtual EMCPError GetSensorModuleMagEnvValue(EMCPMagEnv * pMagEnvValue, MCPSensorModuleHandle_t sensorModuleHandle) = 0;
+ virtual EMCPError GetSesnorModulePacketRate(float * pRate, MCPSensorModuleHandle_t sensorModuleHandle) = 0;
+ virtual EMCPError GetSensorModuleSerialNumber(const char ** serialNumber, MCPSensorModuleHandle_t sensorModuleHandle) = 0;
+ virtual EMCPError GetSensorModuleFirmwareVersionString(const char ** firmwareVersion, MCPSensorModuleHandle_t sensorModuleHandle) = 0;
+ virtual EMCPError GetSensorModuleFirmwareVersion(uint32_t * major, uint32_t * minor, uint32_t * revision, uint32_t * build, MCPSensorModuleHandle_t sensorModuleHandle) = 0;
+ };
+ static const char * IMCPSensorModule_Version = "IMCPSensorModule_002";
+ typedef uint64_t MCPBodyPartHandle_t;
+ class IMCPBodyPart
+ {
+ public:
+ virtual EMCPError GetJointPosition(float * x, float * y, float * z, MCPBodyPartHandle_t bodyPartHandle) = 0;
+ virtual EMCPError GetJointDisplacementSpeed(float * x, float * y, float * z, MCPBodyPartHandle_t bodyPartHandle) = 0;
+ virtual EMCPError GetBodyPartPosture(float * x, float * y, float * z, float * w, MCPBodyPartHandle_t bodyPartHandle) = 0;
+ };
+ static const char * IMCPBodyPart_Version = "IMCPBodyPart_001";
+ enum EMCPGroundingState
+ {
+ GroundingState_Grounding=0,
+ GroundingState_Flying=1
+ };
+ typedef uint64_t MCPJointHandle_t;
+ class IMCPJoint
+ {
+ public:
+ virtual EMCPError GetJointName(const char ** ppStr, MCPJointHandle_t ulJointHandle) = 0;
+ virtual EMCPError GetJointLocalRotation(float * x, float * y, float * z, float * w, MCPJointHandle_t ulJointHandle) = 0;
+ virtual EMCPError GetJointLocalRotationByEuler(float * x, float * y, float * z, MCPJointHandle_t ulJointHandle) = 0;
+ virtual EMCPError GetJointLocalPosition(float * x, float * y, float * z, MCPJointHandle_t ulJointHandle) = 0;
+ virtual EMCPError GetJointDefaultLocalPosition(float * x, float * y, float * z, MCPJointHandle_t ulJointHandle) = 0;
+ virtual EMCPError GetJointChild(MCPJointHandle_t * pJointHandle, uint32_t * punSizeOfJointHandle, MCPJointHandle_t ulJointHandle) = 0;
+ virtual EMCPError GetJointBodyPart(MCPBodyPartHandle_t * pBodyPartHandle, MCPJointHandle_t ulJointHandle) = 0;
+ virtual EMCPError GetJointSensorModule(MCPSensorModuleHandle_t * pSensorModuleHandle, MCPJointHandle_t ulJointHandle) = 0;
+ virtual EMCPError GetJointTag(EMCPJointTag * pJointTag, MCPJointHandle_t ulJointHandle) = 0;
+ virtual EMCPError GetJointNameByTag(const char ** ppStr, EMCPJointTag jointTag) = 0;
+ virtual EMCPError GetJointChildJointTag(EMCPJointTag * pJointTag, uint32_t * punSizeOfJointTag, EMCPJointTag jointTag) = 0;
+ virtual EMCPError GetJointParentJointTag(EMCPJointTag * pJointTag, EMCPJointTag jointTag) = 0;
+ virtual EMCPError GetJointGroundingState(EMCPGroundingState * pGroundingState, MCPJointHandle_t ulJointHandle) = 0;
+ virtual EMCPError GetJointGroundablePoints(float * pointsPostion, uint32_t * numberOfPoints, uint32_t * plowest_index, MCPJointHandle_t ulJointHandle) = 0;
+ virtual EMCPError GetJointGlobalRotation(float * x, float * y, float * z, float * w, MCPJointHandle_t ulJointHandle) = 0;
+ virtual EMCPError GetJointGlobalPosition(float * x, float * y, float * z, MCPJointHandle_t ulJointHandle) = 0;
+ };
+ static const char * IMCPJoint_Version = "IMCPJoint_004";
+ typedef uint64_t MCPAvatarHandle_t;
+ class IMCPAvatar
+ {
+ public:
+ virtual EMCPError GetAvatarIndex(uint32_t * index, MCPAvatarHandle_t ulAvatarHandle) = 0;
+ virtual EMCPError GetAvatarRootJoint(MCPJointHandle_t * pJointHandle, MCPAvatarHandle_t ulAvatarHandle) = 0;
+ virtual EMCPError GetAvatarJoints(MCPJointHandle_t * pJointHandle, uint32_t * punSizeOfJointHandle, MCPAvatarHandle_t ulAvatarHandle) = 0;
+ virtual EMCPError GetAvatarJointByName(const char * name, MCPJointHandle_t * pJointHandle, MCPAvatarHandle_t ulAvatarHandle) = 0;
+ virtual EMCPError GetAvatarName(const char ** ppStr, MCPAvatarHandle_t ulAvatarHandle) = 0;
+ virtual EMCPError GetAvatarRigidBodies(MCPRigidBodyHandle_t * vRigidBodies, uint32_t * punSizeOfRigidBodies, MCPAvatarHandle_t ulAvatarHandle) = 0;
+ virtual EMCPError GetAvatarJointHierarchy(const char ** ppStr) = 0;
+ virtual EMCPError GetAvatarPostureIndex(uint32_t * postureIndex, MCPAvatarHandle_t ulAvatarHandle) = 0;
+ virtual EMCPError GetAvatarPostureTimeCode(uint32_t * hour, uint32_t * minute, uint32_t * second, uint32_t * frame, uint32_t * rate, MCPAvatarHandle_t ulAvatarHandle) = 0;
+ virtual EMCPError GetAvatarPostureTime(uint32_t * hour, uint32_t * minute, uint32_t * second, uint32_t * millisecond, MCPAvatarHandle_t ulAvatarHandle) = 0;
+ virtual EMCPError GetAvatarPosturePtpTime(uint32_t * second, uint32_t * nanosecond, MCPAvatarHandle_t ulAvatarHandle) = 0;
+ };
+ static const char * IMCPAvatar_Version = "IMCPAvatar_005";
+ typedef uint64_t MCPMarkerHandle_t;
+ class IMCPMarker
+ {
+ public:
+ virtual EMCPError GetMarkerPosition(float * x, float * y, float * z, MCPMarkerHandle_t handle) = 0;
+ };
+ static const char * IMCPMarker_Version = "IMCPMarker_001";
+ typedef uint64_t MCPPWRHandle_t;
+ class IMCPPWR
+ {
+ public:
+ virtual EMCPError GetPWRId(uint32_t * id, MCPPWRHandle_t handle) = 0;
+ virtual EMCPError GetPWRStatus(int * status, MCPPWRHandle_t handle) = 0;
+ virtual EMCPError GetPWRPosition(float * x, float * y, float * z, MCPPWRHandle_t handle) = 0;
+ virtual EMCPError GetPWRQuaternion(float * x, float * y, float * z, float * w, MCPPWRHandle_t handle) = 0;
+ };
+ static const char * IMCPPWR_Version = "IMCPPWR_001";
+ typedef uint64_t MCPAliceBusHandle_t;
+ class IMCPAliceHub
+ {
+ public:
+ virtual EMCPError GetSensorModuleList(MCPSensorModuleHandle_t * pHandles, uint32_t * nHandles) = 0;
+ virtual EMCPError GetSensorModuleTimestamp(uint64_t * timestamp) = 0;
+ virtual EMCPError GetMarkerList(MCPMarkerHandle_t * pHandles, uint32_t * nHandles) = 0;
+ virtual EMCPError GetMarkerTimestamp(uint64_t * timestamp) = 0;
+ virtual EMCPError GetRigidBodyList(MCPRigidBodyHandle_t * pHandles, uint32_t * nHandles) = 0;
+ virtual EMCPError GetRigidBodyTimestamp(uint64_t * timestamp) = 0;
+ virtual EMCPError GetPWRList(MCPPWRHandle_t * pHandles, uint32_t * nHandles) = 0;
+ virtual EMCPError GetPWRTimestamp(uint64_t * timestamp) = 0;
+ };
+ static const char * IMCPAliceHub_Version = "IMCPAliceHub_001";
+ enum EStartCaptureFlag
+ {
+ StartCaptureFlag_EnablePtp=1
+ };
+ enum EMCPCommand
+ {
+ CommandStartCapture=0,
+ CommandStopCapture=1,
+ CommandZeroPosition=2,
+ CommandCalibrateMotion=3,
+ CommandStartRecored=4,
+ CommandStopRecored=5,
+ CommandResumeOriginalPosture=6,
+ CommandClearZeroMotionDrift=7,
+ CommandResumeOriginalHandsPosture=8,
+ CommandPilotSimulationDriving=9
+ };
+ enum EMCPCalibrateMotionFlag
+ {
+ CalibrateMotionFlag_AutoNextStep=0,
+ CalibrateMotionFlag_ManualNextStep=1
+ };
+ enum EMCPCalibrateMotionOperation
+ {
+ CalibrateMotionOperation_Next=0,
+ CalibrateMotionOperation_Cancel=1
+ };
+ enum EMCPCommandStopCatpureExtraFlag
+ {
+ StopCatpureExtraFlag_SensorsModulesPowerOff=0,
+ StopCatpureExtraFlag_SensorsModulesHibernate=1
+ };
+ enum EMCPCommandExtraLong
+ {
+ CommandExtraLong_DeviceRadio=0,
+ CommandExtraLong_AvatarName=1,
+ CommandExtraLong_Extra0=0,
+ CommandExtraLong_Extra1=1,
+ CommandExtraLong_Extra2=2,
+ CommandExtraLong_Extra3=3
+ };
+ enum EMCPCommandProgress
+ {
+ CommandProgress_CalibrateMotion=0
+ };
+ enum EMCPRecoredFlag
+ {
+ RecoredFlag_AutoRecalculating=1,
+ RecoredFlag_AutoExportingBvh=2
+ };
+ typedef uint64_t MCPCommandHandle_t;
+ class IMCPCommand
+ {
+ public:
+ virtual EMCPError CreateCommand(uint32_t cmd, MCPCommandHandle_t * handle_) = 0;
+ virtual EMCPError SetCommandExtraFlags(uint32_t extraFlags, MCPCommandHandle_t handle_) = 0;
+ virtual EMCPError SetCommandExtraLong(uint32_t extraLongIndex, intptr_t extraLong, MCPCommandHandle_t handle_) = 0;
+ virtual EMCPError GetCommandResultMessage(const char ** pMsg, MCPCommandHandle_t handle_) = 0;
+ virtual EMCPError GetCommandResultCode(uint32_t * pResCode, MCPCommandHandle_t handle_) = 0;
+ virtual EMCPError GetCommandProgress(uint32_t progress, intptr_t extra, MCPCommandHandle_t handle_) = 0;
+ virtual EMCPError DestroyCommand(MCPCommandHandle_t handle_) = 0;
+ virtual EMCPError DuplicateCommand(MCPCommandHandle_t * duplicateCmd, MCPCommandHandle_t handle_) = 0;
+ virtual EMCPError AddCommandExtraLong(uint32_t extraLongIndex, intptr_t extraLong, MCPCommandHandle_t handle_) = 0;
+ virtual EMCPError GetCommandExtraLong(uint32_t extraLongIndex, intptr_t * pExtraLong, MCPCommandHandle_t handle_) = 0;
+ virtual EMCPError RemoveCommandExtraLong(uint32_t extraLongIndex, MCPCommandHandle_t handle_) = 0;
+ virtual EMCPError GetCommandTag(uint32_t * pTag, MCPCommandHandle_t handle_) = 0;
+ };
+ static const char * IMCPCommand_Version = "IMCPCommand_003";
+ enum EMCPCalibrateMotionProgressStep
+ {
+ CalibrateMotionProgressStep_Prepare=0,
+ CalibrateMotionProgressStep_Countdown=1,
+ CalibrateMotionProgressStep_Progress=2
+ };
+ typedef uint64_t MCPCalibrateMotionProgressHandle_t;
+ class IMCPCalibrateMotionProgress
+ {
+ public:
+ virtual EMCPError GetCalibrateMotionProgressCountOfSupportPoses(uint32_t * pCount, MCPCalibrateMotionProgressHandle_t handle_) = 0;
+ virtual EMCPError GetCalibrateMotionProgressNameOfSupportPose(char * pName, uint32_t * pLenOfName, uint32_t index, MCPCalibrateMotionProgressHandle_t handle_) = 0;
+ virtual EMCPError GetCalibrateMotionProgressStepOfPose(uint32_t * pStep, const char * name, MCPCalibrateMotionProgressHandle_t handle_) = 0;
+ virtual EMCPError GetCalibrateMotionProgressCountdownOfPose(uint32_t * pCountdown, const char * name, MCPCalibrateMotionProgressHandle_t handle_) = 0;
+ virtual EMCPError GetCalibrateMotionProgressProgressOfPose(uint32_t * pProgress, const char * name, MCPCalibrateMotionProgressHandle_t handle_) = 0;
+ virtual EMCPError GetCalibrateMotionProgressStepOfCurrentPose(uint32_t * pStep, char * pName, uint32_t * pLenOfName, MCPCalibrateMotionProgressHandle_t handle_) = 0;
+ virtual EMCPError GetCalibrateMotionProgressCountdownOfCurrentPose(uint32_t * pCountdown, char * pName, uint32_t * pLenOfName, MCPCalibrateMotionProgressHandle_t handle_) = 0;
+ virtual EMCPError GetCalibrateMotionProgressProgressOfCurrentPose(uint32_t * pProgress, char * pName, uint32_t * pLenOfName, MCPCalibrateMotionProgressHandle_t handle_) = 0;
+ };
+ static const char * IMCPCalibrateMotionProgress_Version = "IMCPCalibrateMotionProgress_001";
+ enum EMCPMasterStatus
+ {
+ MasterStatus_Online=0,
+ MasterStatus_Offline=1
+ };
+ typedef uint64_t MCPSystemHandle_t;
+ class IMCPSystem
+ {
+ public:
+ virtual EMCPError GetMasterVersion(const char ** pstr, MCPSystemHandle_t handle) = 0;
+ virtual EMCPError GetMasterSerialNumber(const char ** pstr, MCPSystemHandle_t handle) = 0;
+ virtual EMCPError GetSensorModules(MCPSensorModuleHandle_t * pHandles, uint32_t * pLenOfHandles, MCPSystemHandle_t handle) = 0;
+ virtual EMCPError GetMasterStatus(EMCPMasterStatus * masterStatus, MCPSystemHandle_t handle) = 0;
+ };
+ static const char * IMCPSystem_Version = "IMCPSystem_001";
+ struct MCPEvent_Reserved_t
+ {
+ uint64_t reserved0;
+ uint64_t reserved1;
+ uint64_t reserved2;
+ uint64_t reserved3;
+ uint64_t reserved4;
+ uint64_t reserved5;
+ };
+ struct MCPEvent_MotionData_t
+ {
+ MCPAvatarHandle_t avatarHandle;
+ };
+ struct MCPEvent_SystemError_t
+ {
+ EMCPError error;
+ uint64_t info0;
+ };
+ struct MCPEvent_SensorModuleData_t
+ {
+ MCPSensorModuleHandle_t _sensorModuleHandle;
+ };
+ struct MCPEvent_TrackerData_t
+ {
+ MCPTrackerHandle_t _trackerHandle;
+ };
+ struct MCPEvent_MarkerData_t
+ {
+ MCPMarkerHandle_t _markerHandle;
+ };
+ struct MCPEvent_PWRData_t
+ {
+ MCPPWRHandle_t _pwrHandle;
+ };
+ enum EMCPNotify
+ {
+ Notify_RecordStarted=0,
+ Notify_RecordStoped=1,
+ Notify_RecordFinished=2,
+ Notify_SystemUpdated=3
+ };
+ struct MCPEvent_NotifyData_t
+ {
+ EMCPNotify _notify;
+ uint64_t _notifyHandle;
+ };
+ enum EMCPReplay
+ {
+ MCPReplay_Response=0,
+ MCPReplay_Running=1,
+ MCPReplay_Result=2
+ };
+ struct MCPEvent_CommandRespond_t
+ {
+ MCPCommandHandle_t _commandHandle;
+ EMCPReplay _replay;
+ };
+ union MCPEventData_t
+ {
+ MCPEvent_Reserved_t reserved;
+ MCPEvent_MotionData_t motionData;
+ MCPEvent_SystemError_t systemError;
+ MCPEvent_SensorModuleData_t sensorModuleData;
+ MCPEvent_TrackerData_t trackerData;
+ MCPEvent_CommandRespond_t commandRespond;
+ MCPEvent_MarkerData_t markerData;
+ MCPEvent_PWRData_t pwrData;
+ MCPEvent_NotifyData_t notifyData;
+ };
+ enum EMCPEventType
+ {
+ MCPEvent_None=0,
+ MCPEvent_AvatarUpdated=256,
+ MCPEvent_RigidBodyUpdated=512,
+ MCPEvent_Error=768,
+ MCPEvent_SensorModulesUpdated=1024,
+ MCPEvent_TrackerUpdated=1280,
+ MCPEvent_CommandReply=1536,
+ MCPEvent_Notify=1792,
+ MCPEvent_AliceIMUUpdated=4096,
+ MCPEvent_AliceRigidbodyUpdated=4097,
+ MCPEvent_AliceTrackerUpdated=4098,
+ MCPEvent_AliceMarkerUpdated=4099
+ };
+ struct MCPEvent_t
+ {
+ uint32_t size;
+ EMCPEventType eventType;
+ double fTimestamp;
+ MCPEventData_t eventData;
+ };
+ enum EMCPBvhRotation
+ {
+ BvhRotation_XYZ=0,
+ BvhRotation_XZY=1,
+ BvhRotation_YXZ=2,
+ BvhRotation_YZX=3,
+ BvhRotation_ZXY=4,
+ BvhRotation_ZYX=5
+ };
+ enum EMCPBvhData
+ {
+ BvhDataType_String=0,
+ BvhDataType_BinaryWithOldFrameHeader=1,
+ BvhDataType_Binary=2,
+ BvhDataType_Mask_LegacyHumanHierarchy=4
+ };
+ enum EMCPBvhTransformation
+ {
+ BvhTransformation_Disable=0,
+ BvhTransformation_Enable=1
+ };
+ typedef uint64_t MCPSettingsHandle_t;
+ class IMCPSettings
+ {
+ public:
+ virtual EMCPError CreateSettings(MCPSettingsHandle_t * pSettingsHandle) = 0;
+ virtual EMCPError DestroySettings(MCPSettingsHandle_t ulSettingsHandle) = 0;
+ virtual EMCPError SetSettingsUDP(uint16_t localPort, MCPSettingsHandle_t ulSettingsHandle) = 0;
+ virtual EMCPError SetSettingsTCP(const char * serverIp, uint16_t serverPort, MCPSettingsHandle_t ulSettingsHandle) = 0;
+ virtual EMCPError SetSettingsBvhRotation(EMCPBvhRotation bvhRotation, MCPSettingsHandle_t ulSettingsHandle) = 0;
+ virtual EMCPError SetSettingsBvhTransformation(EMCPBvhTransformation bvhTransformation, MCPSettingsHandle_t ulSettingsHandle) = 0;
+ virtual EMCPError SetSettingsBvhData(EMCPBvhData bvhData, MCPSettingsHandle_t ulSettingsHandle) = 0;
+ virtual EMCPError SetSettingsCalcData(MCPSettingsHandle_t ulSettingsHandle) = 0;
+ virtual EMCPError SetSettingsUDPServer(const char * serverIp, uint16_t serverPort, MCPSettingsHandle_t ulSettingsHandle) = 0;
+ virtual EMCPError SetSettingsUDPEx(const char * localIp, uint16_t localPort, MCPSettingsHandle_t ulSettingsHandle) = 0;
+ };
+ static const char * IMCPSettings_Version = "IMCPSettings_001";
+ enum EMCPUpVector
+ {
+ UpVector_XAxis=1,
+ UpVector_YAxis=2,
+ UpVector_ZAxis=3
+ };
+ enum EMCPFrontVector
+ {
+ FrontVector_ParityEven=1,
+ FrontVector_ParityOdd=2
+ };
+ enum EMCPCoordSystem
+ {
+ CoordSystem_RightHanded=0,
+ CoordSystem_LeftHanded=1
+ };
+ enum EMCPRotatingDirection
+ {
+ RotatingDirection_Clockwise=0,
+ RotatingDirection_CounterClockwise=1
+ };
+ enum EMCPPreDefinedRenderSettings
+ {
+ PreDefinedRenderSettings_Default=0,
+ PreDefinedRenderSettings_UnrealEngine=1,
+ PreDefinedRenderSettings_Unity3D=2,
+ PreDefinedRenderSettings_Count=3
+ };
+ enum EMCPUnit
+ {
+ Unit_Centimeter=0,
+ Uint_Meter=1
+ };
+ typedef uint64_t MCPRenderSettingsHandle_t;
+ class IMCPRenderSettings
+ {
+ public:
+ virtual EMCPError CreateRenderSettings(MCPRenderSettingsHandle_t * pRenderSettings) = 0;
+ virtual EMCPError GetPreDefRenderSettings(EMCPPreDefinedRenderSettings preDefinedRenderSettings, MCPRenderSettingsHandle_t * pRenderSettings) = 0;
+ virtual EMCPError SetUpVector(EMCPUpVector upVector, int sign, MCPRenderSettingsHandle_t renderSettings) = 0;
+ virtual EMCPError GetUpVector(EMCPUpVector * pUpVector, int * sign, MCPRenderSettingsHandle_t renderSettings) = 0;
+ virtual EMCPError SetFrontVector(EMCPFrontVector frontVector, int sign, MCPRenderSettingsHandle_t renderSettings) = 0;
+ virtual EMCPError GetFrontVector(EMCPFrontVector * pFrontVector, int * sign, MCPRenderSettingsHandle_t renderSettings) = 0;
+ virtual EMCPError SetCoordSystem(EMCPCoordSystem coordSystem, MCPRenderSettingsHandle_t renderSettings) = 0;
+ virtual EMCPError GetCoordSystem(EMCPCoordSystem * pCoordSystem, MCPRenderSettingsHandle_t renderSettings) = 0;
+ virtual EMCPError SetRotatingDirection(EMCPRotatingDirection rotatingDirection, MCPRenderSettingsHandle_t renderSettings) = 0;
+ virtual EMCPError GetRotatingDirection(EMCPRotatingDirection * pRotatingDirection, MCPRenderSettingsHandle_t renderSettings) = 0;
+ virtual EMCPError SetUnit(EMCPUnit mcpUnit, MCPRenderSettingsHandle_t renderSettings) = 0;
+ virtual EMCPError GetUnit(EMCPUnit * mcpUnit, MCPRenderSettingsHandle_t renderSettings) = 0;
+ virtual EMCPError DestroyRenderSettings(MCPRenderSettingsHandle_t renderSettings) = 0;
+ };
+ static const char * IMCPRenderSettings_Version = "IMCPRenderSettings_001";
+ typedef void ( * MCPEventHandleProc ) ( const MCPEvent_t * ev , void * custom );
+ typedef uint64_t MCPApplicationHandle_t;
+ class IMCPApplication
+ {
+ public:
+ virtual EMCPError CreateApplication(MCPApplicationHandle_t * ulApplicationHandle) = 0;
+ virtual EMCPError DestroyApplication(MCPApplicationHandle_t ulApplicationHandle) = 0;
+ virtual EMCPError SetApplicationSettings(MCPSettingsHandle_t ulSettingsHandle, MCPApplicationHandle_t ulApplicationHandle) = 0;
+ virtual EMCPError SetApplicationRenderSettings(MCPRenderSettingsHandle_t ulRenderSettings, MCPApplicationHandle_t ulApplicationHandle) = 0;
+ virtual EMCPError OpenApplication(MCPApplicationHandle_t ulApplicationHandle) = 0;
+ virtual EMCPError EnableApplicationCacheEvents(MCPApplicationHandle_t ulApplicationHandle) = 0;
+ virtual EMCPError DisableApplicationCacheEvents(MCPApplicationHandle_t ulApplicationHandle) = 0;
+ virtual EMCPError ApplicationCacheEventsIsEnabled(bool * isEnabled, MCPApplicationHandle_t ulApplicationHandle) = 0;
+ virtual EMCPError CloseApplication(MCPApplicationHandle_t ulApplicationHandle) = 0;
+ virtual EMCPError GetApplicationRigidBodies(MCPRigidBodyHandle_t * pRigidBodyHandle, uint32_t * punRigidBodyHandleSize, MCPApplicationHandle_t ulApplicationHandle) = 0;
+ virtual EMCPError GetApplicationAvatars(MCPAvatarHandle_t * pAvatarHandle, uint32_t * punAvatarHandle, MCPApplicationHandle_t ulApplicationHandle) = 0;
+ virtual EMCPError PollApplicationNextEvent(MCPEvent_t * pEvent, uint32_t * punSizeOfEvent, MCPApplicationHandle_t ulApplicationHandle) = 0;
+ virtual EMCPError GetApplicationSensorModules(MCPSensorModuleHandle_t * pSensorModuleHandle, uint32_t * punSensorModuleHandle, MCPApplicationHandle_t ulApplicationHandle) = 0;
+ virtual EMCPError GetApplicationTrackers(MCPTrackerHandle_t * pTrackerHandle, uint32_t * punTrackerHandle, MCPApplicationHandle_t ulApplicationHandle) = 0;
+ virtual EMCPError QueuedServerCommand(MCPCommandHandle_t cmdHandle, MCPApplicationHandle_t ulApplicationHandle) = 0;
+ virtual EMCPError RegisterEventHandler(MCPEventHandleProc eventHandleProc, intptr_t userData, MCPApplicationHandle_t ulApplicationHandle) = 0;
+ virtual EMCPError UnregisterEventHandler(MCPEventHandleProc eventHandleProc, intptr_t * userData, MCPApplicationHandle_t ulApplicationHandle) = 0;
+ virtual EMCPError GetAppplicationSystem(MCPSystemHandle_t * pSystemHandle, MCPApplicationHandle_t ulApplicationHandle) = 0;
+ };
+ static const char * IMCPApplication_Version = "IMCPApplication_004";
+ typedef uint64_t MCPRecordNotifyHandle_t;
+ class IMCPRecordNotify
+ {
+ public:
+ virtual EMCPError DuplicateRecordNotify(MCPRecordNotifyHandle_t * duplicatedRecordNotifyHandle, MCPRecordNotifyHandle_t recordNotifyHandle) = 0;
+ virtual EMCPError RecordNotifyGetTakeName(const char ** takeName, MCPRecordNotifyHandle_t recordNotifyHandle) = 0;
+ virtual EMCPError RecordNotifyGetTakePath(const char ** takePath, MCPRecordNotifyHandle_t recordNotifyHandle) = 0;
+ virtual EMCPError RecordNotifyGetTakeSaveDir(const char ** takeSaveDir, MCPRecordNotifyHandle_t recordNotifyHandle) = 0;
+ virtual EMCPError RecordNotifyGetTakeFileSuffix(const char ** takeFileSuffix, MCPRecordNotifyHandle_t recordNotifyHandle) = 0;
+ virtual EMCPError DestroyRecordNotify(MCPRecordNotifyHandle_t recordNotifyHandle) = 0;
+ };
+ static const char * IMCPRecordNotify_Version = "IMCPRecordNotify_001";
+ enum EMCPRobotJointType
+ {
+ JointLeftShoulder_Pitch=0,
+ JointLeftShoulder_Roll=1,
+ JointLeftShoulder_Yaw=2,
+ JointLeftElbow=3,
+ JointLeftWrist_Yaw=4,
+ JointLeftWrist_Roll=5,
+ JointLeftWrist_Pitch=6,
+ JointRightShoulder_Pitch=7,
+ JointRightShoulder_Roll=8,
+ JointRightShoulder_Yaw=9,
+ JointRightElbow=10,
+ JointRightWrist_Yaw=11,
+ JointRightWrist_Roll=12,
+ JointRightWrist_Pitch=13,
+ JointLeftHip_Yaw=14,
+ JointLeftHip_Pitch=15,
+ JointLeftHip_Roll=16,
+ JointLeftKnee=17,
+ JointLeftAnkle_Pitch=18,
+ JointLeftAnkle_Roll=19,
+ JointRightHip_Yaw=20,
+ JointRightHip_Pitch=21,
+ JointRightHip_Roll=22,
+ JointRightKnee=23,
+ JointRightAnkle_Pitch=24,
+ JointRightAnkle_Roll=25,
+ JointSpine_Yaw=26,
+ JointSpine_Roll=27,
+ JointSpine_Pitch=28,
+ JointHead_Yaw=29,
+ JointHead_Roll=30,
+ JointHead_Pitch=31,
+ JointCount=32
+ };
+ typedef uint64_t MCPRobotHandle_t;
+ class IMCPRobot
+ {
+ public:
+ virtual EMCPError CreateRobot(const char * jsonConfigStr, MCPRobotHandle_t * pHandle) = 0;
+ virtual EMCPError SetRobotFPS(int fps, MCPRobotHandle_t handle) = 0;
+ virtual EMCPError UpdateRobot(MCPAvatarHandle_t avatarHandle, MCPRobotHandle_t handle) = 0;
+ virtual EMCPError GetRobotRawJointAngle(EMCPRobotJointType jointType, float * value, MCPRobotHandle_t handle) = 0;
+ virtual EMCPError GetRobotRetargetJointAngle(const char * jointName, float * value, MCPRobotHandle_t handle) = 0;
+ virtual EMCPError GetRobotRosFrameJson(const char ** jsonStr, bool compress, MCPRobotHandle_t handle) = 0;
+ virtual EMCPError GetRobotRootRotation(float * x, float * y, float * z, float * w, MCPRobotHandle_t handle) = 0;
+ virtual EMCPError GetRobotRootPosition(float * x, float * y, float * z, MCPRobotHandle_t handle) = 0;
+ virtual EMCPError DestroyRobot(MCPRobotHandle_t handle) = 0;
+ virtual EMCPError RunRobotStep(MCPRobotHandle_t handle) = 0;
+ virtual EMCPError RunRobotStep1(float fixedDelta, MCPRobotHandle_t handle) = 0;
+ virtual EMCPError GetRobotSlideSpeed(float * value, MCPRobotHandle_t handle) = 0;
+ virtual EMCPError GetRobotSlideHeight(float * value, MCPRobotHandle_t handle) = 0;
+ };
+ static const char * IMCPRobot_Version = "IMCPRobot_001";
+ MCP_INTERFACE EMCPError MCP_CALLTYPE MCPGetGenericInterface(const char * pchInterfaceVersion, void ** ppInterface);
+ MCP_INTERFACE void MCP_CALLTYPE MCPGetMocapApiVersion(uint32_t * major, uint32_t * minor, uint32_t * build, uint32_t * revision);
+ MCP_INTERFACE const char * MCP_CALLTYPE MCPGetMocapApiVersionString();
+}
+#endif // end _NOITOM_MOCAPAPI_H
diff --git a/src/plugins/noitom_mocap/third_party/mocap_api/lib/amd64/MocapApi.dll b/src/plugins/noitom_mocap/third_party/mocap_api/lib/amd64/MocapApi.dll
new file mode 100644
index 000000000..a32ef4d17
Binary files /dev/null and b/src/plugins/noitom_mocap/third_party/mocap_api/lib/amd64/MocapApi.dll differ
diff --git a/src/plugins/noitom_mocap/third_party/mocap_api/lib/amd64/MocapApi.lib b/src/plugins/noitom_mocap/third_party/mocap_api/lib/amd64/MocapApi.lib
new file mode 100644
index 000000000..d55783e0d
Binary files /dev/null and b/src/plugins/noitom_mocap/third_party/mocap_api/lib/amd64/MocapApi.lib differ
diff --git a/src/plugins/noitom_mocap/third_party/mocap_api/lib/arm64/libMocapApi.so b/src/plugins/noitom_mocap/third_party/mocap_api/lib/arm64/libMocapApi.so
new file mode 100644
index 000000000..cc1880c08
Binary files /dev/null and b/src/plugins/noitom_mocap/third_party/mocap_api/lib/arm64/libMocapApi.so differ
diff --git a/src/plugins/noitom_mocap/third_party/mocap_api/lib/x86_64/libMocapApi.so b/src/plugins/noitom_mocap/third_party/mocap_api/lib/x86_64/libMocapApi.so
new file mode 100644
index 000000000..b8ac5caa2
Binary files /dev/null and b/src/plugins/noitom_mocap/third_party/mocap_api/lib/x86_64/libMocapApi.so differ
diff --git a/src/plugins/noitom_mocap/third_party/mocap_api/mocap_api.py b/src/plugins/noitom_mocap/third_party/mocap_api/mocap_api.py
new file mode 100644
index 000000000..53654f63f
--- /dev/null
+++ b/src/plugins/noitom_mocap/third_party/mocap_api/mocap_api.py
@@ -0,0 +1,1799 @@
+from ctypes import *
+from collections import namedtuple
+from platform import *
+import time
+import os
+from pathlib import Path
+
+__version__ = "0.1.3"
+
+architecture = machine()
+mocap_file_path = None
+mocap_file_names = []
+
+if architecture == "arm64" or architecture == "aarch64":
+ mocap_file_path = 'lib/arm64'
+ mocap_file_names = ['libMocapApi.so']
+elif architecture == "x86_64":
+ mocap_file_path = 'lib/x86_64'
+ mocap_file_names = ['libMocapApi.so']
+elif architecture == "AMD64":
+ mocap_file_path = "lib/amd64"
+ mocap_file_names = ["MocapApi.dll", "MocapApidll"]
+else:
+ raise Exception("Unsupported architecture")
+
+package_dir = Path(__file__).resolve().parent
+candidate_paths = []
+
+for mocap_file_name in mocap_file_names:
+ candidate_paths.append(package_dir / mocap_file_path / mocap_file_name)
+ candidate_paths.append(package_dir.parent / 'build' / 'lib' / 'mocap_api' / mocap_file_path / mocap_file_name)
+
+mocap_lib_file = None
+for candidate in candidate_paths:
+ if candidate.exists():
+ mocap_lib_file = candidate
+ break
+
+if mocap_lib_file is None:
+ checked_paths = '\n'.join(str(path) for path in candidate_paths)
+ raise FileNotFoundError(f'Could not locate Mocap API library. Checked:\n{checked_paths}')
+
+if os.name == 'nt':
+ os.add_dll_directory(str(mocap_lib_file.parent))
+
+print(f'mocap_lib_file={mocap_lib_file}')
+
+MocapApi = cdll.LoadLibrary(str(mocap_lib_file))
+
+MCPError = namedtuple('EMCPError', [
+ 'NoError',
+ 'MoreEvent',
+ 'InsufficientBuffer',
+ 'InvalidObject',
+ 'InvalidHandle',
+ 'InvalidParameter',
+ 'NotSupported',
+ 'IgnoreUDPSettings',
+ 'IgnoreTCPSettings',
+ 'IgnoreBvhSettings',
+ 'JointNotFound',
+ 'WithoutTransformation',
+ 'NoneMessage',
+ 'NoneParent',
+ 'NoneChild',
+ 'AddressInUse'
+])._make(range(16))
+
+MCPJointTag = namedtuple('EMCPJointTag', [
+ 'Hips',
+ 'RightUpLeg',
+ 'RightLeg',
+ 'RightFoot',
+ 'LeftUpLeg',
+ 'LeftLeg',
+ 'LeftFoot',
+ 'Spine',
+ 'Spine1',
+ 'Spine2',
+ 'Neck',
+ 'Neck1',
+ 'Head',
+ 'RightShoulder',
+ 'RightArm',
+ 'RightForeArm',
+ 'RightHand',
+ 'RightHandThumb1',
+ 'RightHandThumb2',
+ 'RightHandThumb3',
+ 'RightInHandIndex',
+ 'RightHandIndex1',
+ 'RightHandIndex2',
+ 'RightHandIndex3',
+ 'RightInHandMiddle',
+ 'RightHandMiddle1',
+ 'RightHandMiddle2',
+ 'RightHandMiddle3',
+ 'RightInHandRing',
+ 'RightHandRing1',
+ 'RightHandRing2',
+ 'RightHandRing3',
+ 'RightInHandPinky',
+ 'RightHandPinky1',
+ 'RightHandPinky2',
+ 'RightHandPinky3',
+ 'LeftShoulder',
+ 'LeftArm',
+ 'LeftForeArm',
+ 'LeftHand',
+ 'LeftHandThumb1',
+ 'LeftHandThumb2',
+ 'LeftHandThumb3',
+ 'LeftInHandIndex',
+ 'LeftHandIndex1',
+ 'LeftHandIndex2',
+ 'LeftHandIndex3',
+ 'LeftInHandMiddle',
+ 'LeftHandMiddle1',
+ 'LeftHandMiddle2',
+ 'LeftHandMiddle3',
+ 'LeftInHandRing',
+ 'LeftHandRing1',
+ 'LeftHandRing2',
+ 'LeftHandRing3',
+ 'LeftInHandPinky',
+ 'LeftHandPinky1',
+ 'LeftHandPinky2',
+ 'LeftHandPinky3',
+ 'Spine3',
+ 'JointsCount',
+])._make([x for x in range(61)])
+
+MCPRigidBodyHandle = c_uint64
+
+class MCPRigidBody(object):
+ IMCPRigidBodyApi_Version = c_char_p(b'PROC_TABLE:IMCPRigidBody_002')
+
+ class MCPRigidBodyApi(Structure):
+ _fields_ = [
+ ('GetRigidBodyRotation',
+ CFUNCTYPE(c_int32, POINTER(c_float), POINTER(c_float), POINTER(c_float), POINTER(c_float), MCPRigidBodyHandle)),
+ ('GetRigidBodyPosition',
+ CFUNCTYPE(c_int32, POINTER(c_float), POINTER(c_float), POINTER(c_float), MCPRigidBodyHandle)),
+ ('GetRigidBodyStatus', CFUNCTYPE(c_int32, POINTER(c_int32), MCPRigidBodyHandle)),
+ ('GetRigidBodyId', CFUNCTYPE(c_int32, POINTER(c_int32), MCPRigidBodyHandle)),
+ ('GetRigidBodyJointTag', CFUNCTYPE(c_int32, POINTER(c_int32), MCPRigidBodyHandle)),
+ ('GetRigidBodyAxisAngle',
+ CFUNCTYPE(c_int32, POINTER(c_float), POINTER(c_float), POINTER(c_float), POINTER(c_float), MCPRigidBodyHandle)),
+ ('GetRigidBodyType', CFUNCTYPE(c_int32, POINTER(c_uint32), MCPRigidBodyHandle)),
+ ('GetRigidBodyName', CFUNCTYPE(c_int32, POINTER(c_char_p), MCPRigidBodyHandle))
+ ]
+
+ api = POINTER(MCPRigidBodyApi)()
+
+ def __init__(self, rigid_body_handle):
+ if not self.api:
+ err = MocapApi.MCPGetGenericInterface(self.IMCPRigidBodyApi_Version, pointer(self.api))
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get MCPSensorModule interface: {0}'.format(MCPError._fields[err]))
+ self.handle = rigid_body_handle
+
+ def get_rotation(self):
+ x = c_float()
+ y = c_float()
+ z = c_float()
+ w = c_float()
+ err = self.api.contents.GetRigidBodyRotation(pointer(x), pointer(y), pointer(z), pointer(w), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get rigid body rotation: {0}'.format(MCPError._fields[err]))
+ return w.value, x.value, y.value, z.value
+
+ def get_position(self):
+ x = c_float()
+ y = c_float()
+ z = c_float()
+ err = self.api.contents.GetRigidBodyPosition(pointer(x), pointer(y), pointer(z), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get rigid body position: {0}'.format(MCPError._fields[err]))
+ return x.value, y.value, z.value
+
+ def get_status(self):
+ status = c_int32()
+ err = self.api.contents.GetRigidBodyStatus(pointer(status), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get rigid body status: {0}'.format(MCPError._fields[err]))
+ return status.value
+
+ def get_id(self):
+ rigid_id = c_int32()
+ err = self.api.contents.GetRigidBodyId(pointer(rigid_id), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get rigid body id: {0}'.format(err))
+ return rigid_id.value
+
+ def get_joint_tag(self):
+ joint_tag = c_int32()
+ err = self.api.contents.GetRigidBodyJointTag(pointer(joint_tag), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get rigid body joint tag: {0}'.format(MCPError._fields[err]))
+ return joint_tag.value
+
+ def get_axis_angle(self):
+ x = c_float()
+ y = c_float()
+ z = c_float()
+ angle = c_float()
+ err = self.api.contents.GetRigidBodyAxisAngle(pointer(x), pointer(y), pointer(z), pointer(angle), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get rigid body axis angle: {0}'.format(MCPError._fields[err]))
+ return x.value, y.value, z.value, angle.value
+
+ def get_type(self):
+ rigid_type = c_uint32()
+ err = self.api.contents.GetRigidBodyType(pointer(rigid_type), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get rigid body type: {0}'.format(MCPError._fields[err]))
+ return rigid_type.value
+
+ def get_name(self):
+ rigid_name = c_char_p()
+ err = self.api.contents.GetRigidBodyName(pointer(rigid_name), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get rigid body name: {0}'.format(MCPError._fields[err]))
+ return rigid_name.value.decode('utf-8') if rigid_name.value else ''
+
+MCPSensorModuleHandle = c_uint64
+
+class MCPSensorModule(object):
+ IMCPSensorModuleApi_Version = c_char_p(b'PROC_TABLE:IMCPSensorModule_001')
+
+ class MCPSensorModuleApi(Structure):
+ _fields_ = [
+ ('GetSensorModulePosture',
+ CFUNCTYPE(c_int32, POINTER(c_float), POINTER(c_float), POINTER(c_float), POINTER(c_float),
+ MCPSensorModuleHandle)),
+ ('GetSensorModuleAngularVelocity',
+ CFUNCTYPE(c_int32, POINTER(c_float), POINTER(c_float), POINTER(c_float), MCPSensorModuleHandle)),
+ ('GetSensorModuleAcceleratedVelocity',
+ CFUNCTYPE(c_int32, POINTER(c_float), POINTER(c_float), POINTER(c_float), MCPSensorModuleHandle)),
+ ('GetSensorModuleId',
+ CFUNCTYPE(c_int32, POINTER(c_int32), MCPSensorModuleHandle))
+ ]
+
+ api = POINTER(MCPSensorModuleApi)()
+
+ def __init__(self, sensor_handle):
+ if not self.api:
+ err = MocapApi.MCPGetGenericInterface(self.IMCPSensorModuleApi_Version, pointer(self.api))
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get MCPSensorModule interface: {0}'.format(MCPError._fields[err]))
+ self.handle = sensor_handle
+
+ def get_posture(self):
+ x = c_float()
+ y = c_float()
+ z = c_float()
+ w = c_float()
+ err = self.api.contents.GetSensorModulePosture(pointer(x), pointer(y), pointer(z), pointer(w), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get sensor module posture: {0}'.format(MCPError._fields[err]))
+ return w.value, x.value, y.value, z.value
+
+ def get_angular_velocity(self):
+ x = c_float()
+ y = c_float()
+ z = c_float()
+ err = self.api.contents.GetSensorModuleAngularVelocity(pointer(x), pointer(y), pointer(z), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get sensor module angular velocity: {0}'.format(MCPError._fields[err]))
+ return x.value, y.value, z.value
+
+ def get_accelerated_velocity(self):
+ x = c_float()
+ y = c_float()
+ z = c_float()
+ err = self.api.contents.GetSensorModuleAcceleratedVelocity(pointer(x), pointer(y), pointer(z), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get sensor module accelerated velocity: {0}'.format(MCPError._fields[err]))
+ return x.value, y.value, z.value
+
+ def get_sensor_module_id(self):
+ sensor_id = c_int32()
+ err = self.api.contents.GetSensorModuleId(pointer(sensor_id), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get sensor module id: {0}'.format(err))
+ return sensor_id.value
+
+MCPBodyPartHandle = c_uint64
+
+class MCPBodyPart(object):
+ IMCPBodyPartApi_Version = c_char_p(b'PROC_TABLE:IMCPBodyPart_001')
+
+ class MCPBodyPartApi(Structure):
+ _fields_ = [
+ ('GetJointPosition', CFUNCTYPE(c_int32, POINTER(c_float), POINTER(c_float), POINTER(c_float), MCPBodyPartHandle)),
+ ('GetJointDisplacementSpeed',
+ CFUNCTYPE(c_int32, POINTER(c_float), POINTER(c_float), POINTER(c_float), MCPBodyPartHandle)),
+ ('GetBodyPartPosture',
+ CFUNCTYPE(c_int32, POINTER(c_float), POINTER(c_float), POINTER(c_float), POINTER(c_float), MCPBodyPartHandle))
+ ]
+
+ api = POINTER(MCPBodyPartApi)()
+
+ def __init__(self, body_part_handle):
+ if not self.api:
+ err = MocapApi.MCPGetGenericInterface(self.IMCPBodyPartApi_Version, pointer(self.api))
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get MCPBodyPartApi interface: {0}'.format(MCPError._fields[err]))
+ self.handle = body_part_handle
+
+ def get_position(self):
+ x = c_float()
+ y = c_float()
+ z = c_float()
+ err = self.api.contents.GetJointPosition(pointer(x), pointer(y), pointer(z), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get joint position:{0}'.format(MCPError._fileds[err]))
+ return x.value, y.value, z.value
+
+ def get_displacement_speed(self):
+ x = c_float()
+ y = c_float()
+ z = c_float()
+ err = self.api.contents.GetJointDisplacementSpeed(pointer(x), pointer(y), pointer(z), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get joint displacement speed: {0}'.format(MCPError._fields[err]))
+ return x.value, y.value, z.value
+
+ def get_posture(self):
+ x = c_float()
+ y = c_float()
+ z = c_float()
+ w = c_float()
+ err = self.api.contents.GetBodyPartPosture(pointer(x), pointer(y), pointer(z), pointer(w), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get body part posture: {0}'.format(MCPError._fields[err]))
+ return w.value, x.value, y.value, z.value
+
+MCPJointHandle = c_uint64
+
+class MCPJoint(object):
+ IMCPJointApi_Version = c_char_p(b"PROC_TABLE:IMCPJoint_003")
+
+ class MCPJointApi(Structure):
+ _fields_ = [
+ ('GetJointName', CFUNCTYPE(c_int32, POINTER(c_char_p), MCPJointHandle)),
+ ('GetJointLocalRotation',
+ CFUNCTYPE(c_int32, POINTER(c_float), POINTER(c_float), POINTER(c_float), POINTER(c_float), MCPJointHandle)),
+ ('GetJointLocalRotationByEuler',
+ CFUNCTYPE(c_int32, POINTER(c_float), POINTER(c_float), POINTER(c_float), MCPJointHandle)),
+ ('GetJointLocalTransformation',
+ CFUNCTYPE(c_int32, POINTER(c_float), POINTER(c_float), POINTER(c_float), MCPJointHandle)),
+ ('GetJointDefaultLocalTransformation',
+ CFUNCTYPE(c_int32, POINTER(c_float), POINTER(c_float), POINTER(c_float), MCPJointHandle)),
+ ('GetJointChild', CFUNCTYPE(c_int32, POINTER(MCPJointHandle), POINTER(c_uint32), MCPJointHandle)),
+ ('GetJointBodyPart', CFUNCTYPE(c_int32, POINTER(MCPBodyPartHandle), MCPJointHandle)),
+ ('GetJointSensorModule', CFUNCTYPE(c_int32, POINTER(MCPSensorModuleHandle), MCPJointHandle)),
+ ('GetJointTag', CFUNCTYPE(c_int32, POINTER(c_int32), MCPJointHandle)),
+ ('GetJointNameByTag', CFUNCTYPE(c_int32, POINTER(c_char_p), c_int32)),
+ ('GetJointChildJointTag', CFUNCTYPE(c_int32, POINTER(c_int32), POINTER(c_uint32), c_int32)),
+ ('GetJointParentJointTag', CFUNCTYPE(c_int32, POINTER(c_int32), c_int32))
+ ]
+
+ api = POINTER(MCPJointApi)()
+
+ def __init__(self, joint_handle):
+ if not self.api:
+ err = MocapApi.MCPGetGenericInterface(self.IMCPJointApi_Version, pointer(self.api))
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get MCPJointApi interface: {0}'.format(MCPError._fields[err]))
+ self.handle = joint_handle
+
+ def get_name(self):
+ joint_name = c_char_p()
+ err = self.api.contents.GetJointName(pointer(joint_name), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get joint name: {0}'.format(MCPError._fields[err]))
+ return str(joint_name.value, encoding='utf8')
+
+ def get_local_rotation(self):
+ x = c_float()
+ y = c_float()
+ z = c_float()
+ w = c_float()
+ err = self.api.contents.GetJointLocalRotation(pointer(x), pointer(y), pointer(z), pointer(w), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get joint local rotation: {0}'.format(MCPError._fields[err]))
+ return w.value, x.value, y.value, z.value
+
+ def get_local_rotation_by_euler(self):
+ x = c_float()
+ y = c_float()
+ z = c_float()
+ err = self.api.contents.GetJointLocalRotationByEuler(pointer(x), pointer(y), pointer(z), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get joint euler: {0}'.format(MCPError._fields[err]))
+ return x.value, y.value, z.value
+
+ def get_local_position(self):
+ x = c_float()
+ y = c_float()
+ z = c_float()
+ err = self.api.contents.GetJointLocalTransformation(pointer(x), pointer(y), pointer(z), self.handle)
+ if err != MCPError.NoError:
+ return None,None,None
+ return x.value, y.value, z.value
+
+ def get_default_local_position(self):
+ x = c_float()
+ y = c_float()
+ z = c_float()
+ err = self.api.contents.GetJointDefaultLocalTransformation(pointer(x), pointer(y), pointer(z), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get joint default local transformation: {0}'.format(MCPError._fields[err]))
+ return x.value, y.value, z.value
+
+ def get_children(self):
+ joint_count = c_uint32()
+ err = self.api.contents.GetJointChild(POINTER(MCPJointHandle)(), pointer(joint_count), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get joint child count: {0}'.format(MCPError._fields[err]))
+ joint_handles = (MCPJointHandle * joint_count.value)()
+ err = self.api.contents.GetJointChild(joint_handles, pointer(joint_count), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get joint child: {0}'.format(MCPError._fields[err]))
+ return [MCPJoint(joint_handles[i]) for i in range(joint_count.value)]
+
+ def get_body_part(self):
+ body_part_handle = MCPBodyPartHandle()
+ err = self.api.contents.GetJointBodyPart(pointer(body_part_handle), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get joint body part: {0}'.format(MCPError._fields[err]))
+ return MCPBodyPart(body_part_handle)
+
+ def get_sensor_module(self):
+ sensor_handle = MCPSensorModuleHandle()
+ err = self.api.contents.GetJointSensorModule(pointer(sensor_handle), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get sensor module: {0}'.format(err))
+ return MCPSensorModule(sensor_handle)
+
+ def get_tag(self):
+ tag = c_int32()
+ err = self.api.contents.GetJointTag(pointer(tag), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get joint tag: {0}'.format(MCPError._fields[err]))
+ return tag.value
+
+ def get_name_by_tag(self, tag):
+ joint_name = c_char_p()
+ err = self.api.contents.GetJointNameByTag(pointer(joint_name), c_int32(tag))
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get joint name of joint tag {0}: {1}'.format(tag, err))
+ return str(joint_name.value, encoding='utf8')
+
+ def get_child_joint_tag(self, tag):
+ joint_count = c_uint32()
+ err = self.api.contents.GetJointChildJointTag(POINTER(c_int32)(), pointer(joint_count), c_int32(tag))
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get joint child joint tag: {0}'.format(MCPError._fields[err]))
+ joints_tag = (c_int32 * joint_count.value)()
+ err = self.api.contents.GetJointChildJointTag(joints_tag, pointer(joint_count), c_int32(tag))
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get joint child joint tag: {0}'.format(MCPError._fields[err]))
+ return [joints_tag[i].value for i in range(joint_count.value)]
+
+ def get_parent_joint_tag(self, tag):
+ joint_tag = c_int32()
+ err = self.api.contents.GetJointParentJointTag(pointer(joint_tag), c_int32(tag))
+ if err != MCPError.NoError:
+ raise RuntimeError('can not get joint parent tag: {0}'.format(MCPError._fields[err]))
+ return joint_tag.value
+
+MCPAvatarHandle = c_uint64
+
+class MCPAvatar(object):
+ IMCPAvatarApi_Version = c_char_p(b'PROC_TABLE:IMCPAvatar_003')
+
+ class MCPAvatarApi(Structure):
+ _fields_ = [
+ ('GetAvatarIndex', CFUNCTYPE(c_int32, POINTER(c_uint32), MCPAvatarHandle)),
+ ('GetAvatarRootJoint', CFUNCTYPE(c_int32, POINTER(MCPJointHandle), MCPAvatarHandle)),
+ ('GetAvatarJoints', CFUNCTYPE(c_int32, POINTER(MCPJointHandle), POINTER(c_uint32), MCPAvatarHandle)),
+ ('GetAvatarJointByName', CFUNCTYPE(c_int32, c_char_p, POINTER(MCPJointHandle), MCPAvatarHandle)),
+ ('GetAvatarName', CFUNCTYPE(c_int32, POINTER(c_char_p), MCPAvatarHandle)),
+ ('GetAvatarRigidBodies', CFUNCTYPE(c_int32, POINTER(MCPRigidBodyHandle), POINTER(c_uint32), MCPAvatarHandle)),
+ ('GetAvatarJointHierarchy', CFUNCTYPE(c_int32, POINTER(c_char_p))),
+ ('GetAvatarPostureIndex', CFUNCTYPE(c_int32, POINTER(c_uint32), MCPAvatarHandle)),
+ ('GetAvatarPostureTimeCode', CFUNCTYPE(c_int32, POINTER(c_uint32), POINTER(c_uint32), POINTER(c_uint32), POINTER(c_uint32), POINTER(c_uint32), MCPAvatarHandle)),
+ ('GetAvatarPostureTime', CFUNCTYPE(c_int32, POINTER(c_uint32), POINTER(c_uint32), POINTER(c_uint32), POINTER(c_uint32), MCPAvatarHandle)),
+ ('GetAvatarPosturePtpTime', CFUNCTYPE(c_int32, POINTER(c_uint32), POINTER(c_uint32), MCPAvatarHandle)),
+ ]
+
+ api = POINTER(MCPAvatarApi)()
+
+ def __init__(self, avatar_handle):
+ if not self.api:
+ err = MocapApi.MCPGetGenericInterface(self.IMCPAvatarApi_Version, pointer(self.api))
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get MCPAvatar interface: {0}'.format(MCPError._fields[err]))
+ self.handle = avatar_handle
+
+ def get_index(self):
+ index = c_uint32(0)
+ err = self.api.contents.GetAvatarIndex(pointer(index), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get avatar index: {0}'.format(MCPError._fields[err]))
+ return index.value
+
+ def get_root_joint(self):
+ joint_handle = MCPJointHandle()
+ err = self.api.contents.GetAvatarRootJoint(pointer(joint_handle), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get avatar root joint: {0}'.format(err))
+ return MCPJoint(joint_handle)
+
+ def get_joints(self):
+ joint_count = c_uint32()
+ err = self.api.contents.GetAvatarJoints(POINTER(MCPJointHandle)(), pointer(joint_count), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get avatar joints: {0}'.format(MCPError._fields[err]))
+ joints_handle = (MCPJointHandle * joint_count.value)()
+ err = self.api.contents.GetAvatarJoints(joints_handle, pointer(joint_count), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get avatar Joints: {0}'.format(MCPError._fields[err]))
+ return [MCPJoint(joints_handle[i]) for i in range(joint_count.value)]
+
+ def get_joint_by_name(self, name):
+ joint_name = c_char_p(bytes(name, encoding='utf8'))
+ joint_handle = MCPJointHandle()
+ err = self.api.contents.GetAvatarJointByName(joint_name, pointer(joint_handle), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get avatar Joints: {0}'.format(MCPError._fields[err]))
+ return MCPJoint(joint_handle)
+
+ def get_name(self):
+ avatar_name = c_char_p()
+ err = self.api.contents.GetAvatarName(pointer(avatar_name), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get avatar name: {0}'.format(MCPError._fields[err]))
+ return str(avatar_name.value, encoding='utf8')
+
+ def get_rigid_bodies(self):
+ rigid_body_count = c_uint32()
+ err = self.api.contents.GetAvatarRigidBodies(POINTER(MCPRigidBodyHandle)(), pointer(rigid_body_count), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get avatar rigid bodies: {0}'.format(MCPError._fields[err]))
+ rigid_body_handles = (MCPRigidBodyHandle * rigid_body_count.value)()
+ err = self.api.contents.GetAvatarRigidBodies(rigid_body_handles, pointer(rigid_body_count), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get avatar rigid bodies: {0}'.format(MCPError._fields[err]))
+ return [MCPRigidBody(rigid_body_handles[i]) for i in range(rigid_body_count.value)]
+
+ def get_joint_hierarchy(self):
+ hierarchy = c_char_p()
+ err = self.api.contents.GetAvatarJointHierarchy(pointer(hierarchy))
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get avatar joint hierarchy: {0}'.format(err))
+ return str(hierarchy.value, encoding='utf8')
+
+ def get_avatar_posture_index(self):
+ postureIndex = c_uint32(0)
+ err = self.api.contents.GetAvatarPostureIndex(byref(postureIndex), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get avatar posture index: {0}'.format(MCPError._fields[err]))
+ return postureIndex.value
+
+ def get_avatar_posture_time(self):
+ hour = c_uint32(0)
+ minute = c_uint32(0)
+ second = c_uint32(0)
+ millisecond = c_uint32(0)
+ err = self.api.contents.GetAvatarPostureTime(byref(hour), byref(minute), byref(second), byref(millisecond), self.handle)
+ print(f"get_avatar_posture_time err: {err}")
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get avatar posture time: {0}'.format(MCPError._fields[err]))
+ return hour.value, minute.value, second.value, millisecond.value
+
+ def get_avatar_posture_time_code(self):
+ hour = c_uint32(0)
+ minute = c_uint32(0)
+ second = c_uint32(0)
+ frame = c_uint32(0)
+ rate = c_uint32(0)
+ err = self.api.contents.GetAvatarPostureTimeCode(byref(hour), byref(minute), byref(second), byref(frame), byref(rate), self.handle)
+ print(f"get_avatar_posture_time_code err: {err}")
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get avatar posture time code: {0}'.format(MCPError._fields[err]))
+ return hour.value, minute.value, second.value, frame.value, rate.value
+
+ def get_avatar_posture_ptp_time(self):
+ second = c_uint32(0)
+ nanosecond = c_uint32(0)
+ err = self.api.contents.GetAvatarPosturePtpTime(byref(second), byref(nanosecond), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get avatar posture ptp time : {0}'.format(MCPError._fields[err]))
+ return second.value, nanosecond.value
+
+
+MCPCalibrateMotionProgressStep = namedtuple('EMCPCalibrateMotionProgressStep', [
+ 'CalibrateMotionProgressStep_Prepare',
+ 'CalibrateMotionProgressStep_Countdown',
+ 'CalibrateMotionProgressStep_Progress'
+])(0, 1, 2)
+
+MCPCalibrateMotionProgressHandle = c_uint64
+class MCPCalibrateMotionProgress(object):
+ IMCPCalibrateMotionProgress_Version = c_char_p(b"PROC_TABLE:IMCPCalibrateMotionProgress_001")
+ class MCPCalibrateMotionProgressApi(Structure):
+ _fields_ = [
+ ('GetCalibrateMotionProgressCountOfSupportPoses', CFUNCTYPE(c_int32, POINTER(c_uint32), MCPCalibrateMotionProgressHandle)),
+ ('GetCalibrateMotionProgressNameOfSupportPose', CFUNCTYPE(c_int32,c_char_p, POINTER(c_uint32), c_int32, MCPCalibrateMotionProgressHandle)),
+ ('GetCalibrateMotionProgressStepOfPose', CFUNCTYPE(c_int32,POINTER(c_uint32),c_char_p, MCPCalibrateMotionProgressHandle)),
+ ('GetCalibrateMotionProgressCountdownOfPose', CFUNCTYPE(c_int32,POINTER(c_uint32),c_char_p, MCPCalibrateMotionProgressHandle)),
+ ('GetCalibrateMotionProgressProgressOfPose', CFUNCTYPE(c_int32,POINTER(c_uint32),c_char_p, MCPCalibrateMotionProgressHandle)),
+ ('GetCalibrateMotionProgressStepOfCurrentPose', CFUNCTYPE(c_int32,POINTER(c_uint32),c_char_p,POINTER(c_uint32), MCPCalibrateMotionProgressHandle)),
+ ('GetCalibrateMotionProgressCountdownOfCurrentPose', CFUNCTYPE(c_int32,POINTER(c_uint32),c_char_p,POINTER(c_uint32), MCPCalibrateMotionProgressHandle)),
+ ('GetCalibrateMotionProgressProgressOfCurrentPose', CFUNCTYPE(c_int32,POINTER(c_uint32),c_char_p,POINTER(c_uint32), MCPCalibrateMotionProgressHandle)),
+ ]
+ api = POINTER(MCPCalibrateMotionProgressApi)()
+
+ def __init__(self, progressHandle):
+ if not self.api:
+ err = MocapApi.MCPGetGenericInterface(self.IMCPCalibrateMotionProgress_Version, pointer(self.api))
+ if err != MCPError.NoError:
+ raise RuntimeError('Cannot get MCPCommandApi {0}'.format(MCPError._fields[err]))
+ self.handle = progressHandle
+
+ def get_count_of_support_poses(self):
+ count = c_uint32()
+ err = self.api.contents.GetCalibrateMotionProgressCountOfSupportPoses(count, self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('GetProgress Count Of Support Poses with error {0}'.format(MCPError._fields[err]))
+ return count.value
+
+ def get_name_of_support_poses(self, index):
+ pName = None
+ pName_ = None
+ pLenOfName = c_uint32(0)
+ err = self.api.contents.GetCalibrateMotionProgressNameOfSupportPose(None, byref(pLenOfName), index, self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('GetProgress Name Of Support Pose with error {0}'.format(MCPError._fields[err]))
+
+ pName_ = create_string_buffer(pLenOfName.value)
+ err_ = self.api.contents.GetCalibrateMotionProgressNameOfSupportPose(pName_, byref(pLenOfName), index, self.handle)
+ if err_ != MCPError.NoError:
+ raise RuntimeError('GetProgress Name Of Support Pose with error {0}'.format(MCPError._fields[err_]))
+
+ pName = pName_.value.decode('utf-8')
+ return pName
+
+ def get_step_current_pose(self):
+ current_step = c_uint32(0)
+ p_len_of_name = c_uint32(0)
+ p_name = None
+
+ err = self.api.contents.GetCalibrateMotionProgressStepOfCurrentPose(byref(current_step), p_name, byref(p_len_of_name), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('GetProgress step Of current Pose with error {0}'.format(MCPError._fields[err]))
+
+ p_name = create_string_buffer(p_len_of_name.value)
+
+ err_ = self.api.contents.GetCalibrateMotionProgressStepOfCurrentPose(byref(current_step), p_name, byref(p_len_of_name), self.handle)
+ if err_ != MCPError.NoError:
+ raise RuntimeError('GetProgress step Of current Pose with error {0}'.format(MCPError._fields[err_]))
+
+ p_name_str = p_name.value.decode('utf-8')
+ return current_step.value, p_name_str
+
+ def get_countdown_current_pose(self):
+ countdown = c_uint32(0)
+ p_len_of_name = c_uint32(0)
+ p_name = None
+
+ err = self.api.contents.GetCalibrateMotionProgressCountdownOfCurrentPose(byref(countdown), p_name, byref(p_len_of_name), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('GetProgress step Of current Pose with error {0}'.format(MCPError._fields[err]))
+
+ p_name = create_string_buffer(p_len_of_name.value)
+
+ err_ = self.api.contents.GetCalibrateMotionProgressCountdownOfCurrentPose(byref(countdown), p_name, byref(p_len_of_name), self.handle)
+ if err_ != MCPError.NoError:
+ raise RuntimeError('GetProgress step Of current Pose with error {0}'.format(MCPError._fields[err_]))
+
+ p_name_str = p_name.value.decode('utf-8')
+ return countdown.value, p_name_str
+
+
+ def get_progress_current_pose(self):
+ progress = c_uint32(0)
+ p_len_of_name = c_uint32(0)
+ p_name = None
+
+ err = self.api.contents.GetCalibrateMotionProgressProgressOfCurrentPose(byref(progress), p_name, byref(p_len_of_name), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('GetProgress step Of current Pose with error {0}'.format(MCPError._fields[err]))
+
+ p_name = create_string_buffer(p_len_of_name.value)
+
+ err_ = self.api.contents.GetCalibrateMotionProgressProgressOfCurrentPose(byref(progress), p_name, byref(p_len_of_name), self.handle)
+ if err_ != MCPError.NoError:
+ raise RuntimeError('GetProgress step Of current Pose with error {0}'.format(MCPError._fields[err_]))
+
+ p_name_str = p_name.value.decode('utf-8')
+ return progress.value, p_name_str
+
+MCPCommandProgress = namedtuple('MCPCommandProgress', [
+ 'CommandProgress_CalibrateMotion'
+])(0)
+
+MCPCommandHandle = c_uint64
+class MCPCommand(object):
+ IMCPCommand_Version = c_char_p(b"PROC_TABLE:IMCPCommand_002")
+ class MCPCommandApi(Structure):
+ _fields_ = [
+ ('CreateCommand', CFUNCTYPE(c_int32, c_int32, POINTER(MCPCommandHandle))),
+ ('SetCommandExtraFlags', CFUNCTYPE(c_int32, c_int32, POINTER(MCPCommandHandle))),
+ ('SetCommandExtraLong', CFUNCTYPE(c_int32, c_int, c_uint64, MCPCommandHandle)),
+ ('GetCommandResultMessage', CFUNCTYPE(c_int32, POINTER(c_char_p), MCPCommandHandle)),
+ ('GetCommandResultCode', CFUNCTYPE(c_int32, POINTER(c_uint32), MCPCommandHandle)),
+ ('GetCommandProgress', CFUNCTYPE(c_int32, c_int32, POINTER(c_ulonglong), MCPCommandHandle)),
+ ('DestroyCommand', CFUNCTYPE(c_int32, MCPCommandHandle)),
+ ]
+ api = POINTER(MCPCommandApi)()
+ def __init__(self):
+ if not self.api:
+ err = MocapApi.MCPGetGenericInterface(self.IMCPCommand_Version, pointer(self.api))
+ if err != MCPError.NoError:
+ raise RuntimeError('Cannot get MCPCommandApi {0}'.format(MCPError._fields[err]))
+ self.handle = MCPCommandHandle()
+ def create_command(self, commandType):
+ err = self.api.contents.CreateCommand(commandType, self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('CreateCommand failed with error {0}'.format(MCPError._fields[err]))
+
+ def set_command_extra_long(self, data=0):
+ # 保存缓冲区引用,防止垃圾回收
+ self._buffer = None
+
+ if isinstance(data, str):
+ # Convert string to bytes
+ data_bytes = data.encode('utf-8')
+ # Create a buffer to hold the bytes
+ data_buffer = create_string_buffer(data_bytes)
+ # 保存缓冲区引用
+ self._buffer = data_buffer
+ # Get the pointer to the buffer
+ # 使用cast将缓冲区转换为POINTER(c_char),然后获取其地址
+ data_ptr = c_uint64(addressof(data_buffer))
+ else:
+ # If data is not a numeric value, convert it to integer first
+ data_ptr = c_uint64(int(data))
+
+ err = self.api.contents.SetCommandExtraLong(c_int(0), data_ptr, self.handle)# 0 is CommandExtraLong_Extra0
+ if err != MCPError.NoError:
+ raise RuntimeError('SetCommandExtraLong failed with error {0}'.format(MCPError._fields[err]))
+
+ def get_result_code(self, commandHandle):
+ pCode = c_uint32()
+ err = self.api.contents.GetCommandResultCode(byref(pCode), commandHandle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Get command result code failed with error {0}'.format(MCPError._fields[err]))
+ return pCode.value
+
+
+ def get_result_message(self, commandHandle):
+ pMsg = c_char_p()
+ err = self.api.contents.GetCommandResultMessage(pointer(pMsg), commandHandle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Get command result message failed with error {0}'.format(MCPError._fields[err]))
+ return str(pMsg.value, encoding='utf8')
+
+ def get_progress(self, commandHandle):
+ pExtraLong = c_ulonglong()
+ err = self.api.contents.GetCommandProgress(MCPCommandProgress.CommandProgress_CalibrateMotion, byref(pExtraLong), commandHandle)
+ if err != MCPError.NoError:
+ raise RuntimeError('GetProgress failed with error {0}'.format(MCPError._fields[err]))
+ return pExtraLong.value
+
+ def destroy_command(self, commandHandle):
+ err = self.api.contents.DestroyCommand(commandHandle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Destroy command failed with error {0}'.format(MCPError._fields[err]))
+
+class MCPEventDataReserved(Structure):
+ _fields_ = [
+ ('reserved0', c_uint64),
+ ('reserved1', c_uint64),
+ ('reserved2', c_uint64),
+ ('reserved3', c_uint64),
+ ('reserved4', c_uint64),
+ ('reserved5', c_uint64),
+ ]
+
+class MCPEventCommandRespond(Structure):
+ _fields_ = [
+ ('_commandHandle', MCPCommandHandle),
+ ('_replay', c_int32)
+ ]
+
+MCPReplay = namedtuple('EMCPReplay', [
+ 'MCPReplay_Response',
+ 'MCPReplay_Running',
+ 'MCPReplay_Result'
+])(0, 1, 2)
+
+
+
+MCPPWRHandle = c_uint64
+class MCPPWR(object):
+ IMCPPWR_Version = c_char_p(b"PROC_TABLE:IMCPPWR_001")
+
+ class MCPPWRApi(Structure):
+ _fields_ = [
+ ('GetPWRId', CFUNCTYPE(c_int32, POINTER(c_uint32), MCPPWRHandle)),
+ ('GetPWRStatus', CFUNCTYPE(c_int32, POINTER(c_int32), MCPPWRHandle)),
+ ('GetPWRPosition', CFUNCTYPE(c_int32, POINTER(c_float), POINTER(c_float), POINTER(c_float), MCPPWRHandle)),
+ ('GetPWRQuaternion', CFUNCTYPE(c_int32, POINTER(c_float), POINTER(c_float), POINTER(c_float), POINTER(c_float), MCPPWRHandle))
+ ]
+ api = POINTER(MCPPWRApi)()
+ def __init__(self, handle):
+ if not self.api:
+ err = MocapApi.MCPGetGenericInterface(self.IMCPPWR_Version, pointer(self.api))
+ if err != MCPError.NoError:
+ raise RuntimeError('Cannot get MCPAliceHubApi: {0}'.format(MCPError._fields[err]))
+ self.handle = handle
+
+ def get_PWR_id(self):
+ p_id = c_uint32()
+ err = self.api.contents.GetPWRId(byref(p_id), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('GetPWRId failed with error {0}'.format(MCPError._fields[err]))
+ return p_id.value
+
+ def get_PWR_status(self):
+ p_status = c_int32()
+ err = self.api.contents.GetPWRStatus(byref(p_status), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('GetPWRStatus failed with error {0}'.format(MCPError._fields[err]))
+ return p_status.value
+
+ def get_PWR_position(self):
+ p_x = c_float()
+ p_y = c_float()
+ p_z = c_float()
+ err = self.api.contents.GetPWRPosition(byref(p_x), byref(p_y), byref(p_z), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('GetPWRPosition failed with error {0}'.format(MCPError._fields[err]))
+ return (p_x.value, p_y.value, p_z.value)
+
+ def get_PWR_quaternion(self):
+ p_x = c_float()
+ p_y = c_float()
+ p_z = c_float()
+ p_w = c_float()
+ err = self.api.contents.GetPWRQuaternion(byref(p_x), byref(p_y), byref(p_z), byref(p_w), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('GetPWRQuaternion failed with error {0}'.format(MCPError._fields[err]))
+ return (p_x.value, p_y.value, p_z.value, p_w.value)
+
+MCPMarkerHandle = c_uint64
+MCPAliceBusHandle = c_uint64
+class MCPAliceHub(object):
+ IMCPAliceHub_Version = c_char_p(b"PROC_TABLE:IMCPAliceHub_001")
+
+ class MCPAliceHubApi(Structure):
+ _fields_ = [
+ ('GetSensorModuleList', CFUNCTYPE(c_int32, POINTER(MCPSensorModuleHandle), POINTER(c_int32))),
+ ('GetSensorModuleTimestamp', CFUNCTYPE(c_int32, POINTER(c_uint64))),
+ ('GetMarkerList', CFUNCTYPE(c_int32, POINTER(MCPMarkerHandle), POINTER(c_int32))),
+ ('GetMarkerTimestamp', CFUNCTYPE(c_int32, POINTER(c_uint64))),
+ ('GetRigidBodyList', CFUNCTYPE(c_int32, POINTER(MCPRigidBodyHandle), POINTER(c_int32))),
+ ('GetRigidBodyTimestamp', CFUNCTYPE(c_int32, POINTER(c_uint64))),
+ ('GetPWRList', CFUNCTYPE(c_int32, POINTER(MCPPWRHandle), POINTER(c_int32))),
+ ('GetPWRTimestamp', CFUNCTYPE(c_int32, POINTER(c_uint64)))
+ ]
+
+ api = POINTER(MCPAliceHubApi)()
+
+ def __init__(self):
+ if not self.api:
+ err = MocapApi.MCPGetGenericInterface(self.IMCPAliceHub_Version, pointer(self.api))
+ if err != MCPError.NoError:
+ raise RuntimeError('Cannot get MCPAliceHubApi: {0}'.format(MCPError._fields[err]))
+ self.handle = MCPAliceBusHandle()
+
+ def get_sensor_module_list(self, count=None):
+ if count is None:
+ count = c_int32()
+ err = self.api.contents.GetSensorModuleList(None, byref(count))
+ if err != MCPError.NoError:
+ raise RuntimeError('Get sensor module list failed with error {0}'.format(MCPError._fields.get(err, "Unknown Error")))
+ return None, count.value
+ else:
+ if isinstance(count, int):
+ count = c_int32(count)
+ handles = (MCPSensorModuleHandle * count.value)()
+ err = self.api.contents.GetSensorModuleList(handles, byref(count))
+ if err != MCPError.NoError:
+ raise RuntimeError('Get sensor module list failed with error {0}'.format(MCPError._fields.get(err, "Unknown Error")))
+ return handles, count.value
+
+ def get_sensor_module_timestamp(self):
+ pTimestamp = c_uint64()
+ err = self.api.contents.GetSensorModuleTimestamp(byref(pTimestamp))
+ if err != MCPError.NoError:
+ raise RuntimeError('Get sensor module timestamp failed with error {0}'.format(MCPError._fields[err]))
+ return pTimestamp.value
+
+ def get_marker_list(self, count=None):
+ if count is None:
+ count = c_int32()
+ err = self.api.contents.GetMarkerList(None, byref(count))
+ if err != MCPError.NoError:
+ raise RuntimeError('Get marker list failed with error {0}'.format(MCPError._fields.get(err, "Unknown Error")))
+ return None, count.value
+ else:
+ if isinstance(count, int):
+ count = c_int32(count)
+ handles = (MCPMarkerHandle * count.value)()
+ err = self.api.contents.GetMarkerList(handles, byref(count))
+ if err != MCPError.NoError:
+ raise RuntimeError('Get marker list failed with error {0}'.format(MCPError._fields.get(err, "Unknown Error")))
+ return handles, count.value
+
+ def get_marker_timestamp(self):
+ pTimestamp = c_uint64()
+ err = self.api.contents.GetMarkerTimestamp(byref(pTimestamp))
+ if err != MCPError.NoError:
+ raise RuntimeError('Get PWR timestamp failed with error {0}'.format(MCPError._fields[err]))
+ return pTimestamp.value
+
+ def get_rigid_body_list(self, count=None):
+ if count is None:
+ count = c_int32()
+ err = self.api.contents.GetRigidBodyList(None, byref(count))
+ if err != MCPError.NoError:
+ raise RuntimeError('Get rigid body list failed with error {0}'.format(MCPError._fields.get(err, "Unknown Error")))
+ return None, count.value
+ else:
+ if isinstance(count, int):
+ count = c_int32(count)
+ handles = (MCPRigidBodyHandle * count.value)()
+ err = self.api.contents.GetRigidBodyList(handles, byref(count))
+ if err != MCPError.NoError:
+ raise RuntimeError('Get rigid body list failed with error {0}'.format(MCPError._fields.get(err, "Unknown Error")))
+ return handles, count.value
+
+ def get_rigid_body_timestamp(self):
+ pTimestamp = c_uint64()
+ err = self.api.contents.GetRigidBodyTimestamp(byref(pTimestamp))
+ if err != MCPError.NoError:
+ raise RuntimeError('Get rigid body timestamp failed with error {0}'.format(MCPError._fields[err]))
+ return pTimestamp.value
+
+ def get_PWR_list(self, count=None):
+ if count is None:
+ count = c_int32()
+ err = self.api.contents.GetPWRList(None, byref(count))
+ if err != MCPError.NoError:
+ raise RuntimeError('Get PWR list failed with error {0}'.format(MCPError._fields.get(err, "Unknown Error")))
+ return None, count.value
+ else:
+ if isinstance(count, int):
+ count = c_int32(count)
+ handles = (MCPPWRHandle * count.value)()
+ err = self.api.contents.GetPWRList(handles, byref(count))
+ if err != MCPError.NoError:
+ raise RuntimeError('Get PWR list failed with error {0}'.format(MCPError._fields.get(err, "Unknown Error")))
+ return handles, count.value
+
+ def get_PWR_timestamp(self):
+ pTimestamp = c_uint64()
+ err = self.api.contents.GetPWRTimestamp(byref(pTimestamp))
+ if err != MCPError.NoError:
+ raise RuntimeError('Get PWR timestamp failed with error {0}'.format(MCPError._fields[err]))
+ return pTimestamp.value
+
+
+MCPSensorModuleHandle = c_uint64
+MCPSystemHandle = c_uint64
+class MCPSystem(object):
+ IMCPSystem_Version = c_char_p(b"PROC_TABLE:IMCPSystem_001")
+
+ class MCPSystemApi(Structure):
+ _fields_ = [
+ ('GetMasterVersion', CFUNCTYPE(c_int32, POINTER(c_char_p), MCPSystemHandle)),
+ ('GetMasterSerialNumber', CFUNCTYPE(c_int32, POINTER(c_char_p), MCPSystemHandle)),
+ ('GetSensorModules', CFUNCTYPE(c_int32, MCPSensorModuleHandle, c_int32, POINTER(MCPSystemHandle))),
+ ]
+
+ api = POINTER(MCPSystemApi)()
+
+ def __init__(self, system_handle):
+ if not self.api:
+ self.api = POINTER(self.MCPSystemApi)()
+ err = MocapApi.MCPGetGenericInterface(self.IMCPSystem_Version, pointer(self.api))
+ if err != MCPError.NoError:
+ raise RuntimeError('Cannot get MCPSystemApi: {0}'.format(MCPError._fields[err]))
+ self.handle = system_handle
+
+ def get_master_version(self):
+ pstr = c_char_p()
+ err = self.api.contents.GetMasterVersion(pointer(pstr), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get master version: {0}'.format(MCPError._fields[err]))
+ return str(pstr.value, encoding='utf8')
+
+ def get_master_serial_number(self):
+ pstr = c_char_p()
+ err = self.api.contents.GetMasterSerialNumber(pointer(pstr), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get master serial number: {0}'.format(MCPError._fields[err]))
+ return str(pstr.value, encoding='utf8')
+
+ def get_sensor_modules(self, sensor_module_type):
+ pstr = c_char_p()
+ err = self.api.contents.GetSensorModules(sensor_module_type, pointer(pstr), pointer(self.handle))
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get sensor modules: {0}'.format(MCPError._fields[err]))
+ return str(pstr.value, encoding='utf8')
+
+MCPEventNotify = namedtuple('EMCPNotify', [
+ 'Notify_RecordStarted',
+ 'Notify_RecordStoped',
+ 'Notify_RecordFinished',
+ 'Notify_SystemUpdated',
+])(0, 1, 2, 3)
+
+class MCPEventNotifyData(Structure):
+ _fields_ = [
+ ('_notify', c_int32),
+ ('_notifyHandle', MCPSystemHandle)
+ ]
+
+MCPTrackerHandle = c_uint64
+
+class MCPEventMotionData(Structure):
+ _fields_ = [
+ ('avatarHandle', MCPAvatarHandle)
+ ]
+
+class MCPEventSystemError(Structure):
+ _fields_ = [
+ ('error', c_int32),
+ ('info0', c_uint64)
+ ]
+
+class MCPEventSensorModuleData(Structure):
+ _fields_ = [
+ ('_sensorModuleHandle', MCPSensorModuleHandle)
+ ]
+
+class MCPEventTrackerData(Structure):
+ _fields_ = [
+ ('_trackerHandle', MCPTrackerHandle)
+ ]
+
+class MCPEventMarkerData(Structure):
+ _fields_ = [
+ ('_markerHandle', MCPMarkerHandle)
+ ]
+
+class MCPEventPWRData(Structure):
+ _fields_ = [
+ ('_pwrHandle', MCPPWRHandle)
+ ]
+
+class MCPEventData(Union):
+ _fields_ = [
+ ('reserved', MCPEventDataReserved),
+ ('motionData', MCPEventMotionData),
+ ('systemError', MCPEventSystemError),
+ ('sensorModuleData', MCPEventSensorModuleData),
+ ('trackerData', MCPEventTrackerData),
+ ('commandRespond', MCPEventCommandRespond),
+ ('markerData', MCPEventMarkerData),
+ ('pwrData', MCPEventPWRData),
+ ('notifyData', MCPEventNotifyData)
+ ]
+
+MCPEventType = namedtuple('EMCPEventType', [
+ 'InvalidEvent',
+ 'AvatarUpdated',
+ 'RigidBodyUpdated',
+ 'Error',
+ 'SensorModulesUpdated',
+ 'TrackerUpdated',
+ "CommandReply",
+ "Notify",
+ "AliceIMUUpdated",
+ "AliceRigidbodyUpdated",
+ "AliceTrackerUpdated",
+ "AliceMarkerUpdated",
+])(0, 256, 512, 768, 1024, 1280,1536, 1792, 4096,4097,4098,4099)
+
+class MCPEvent(Structure):
+ _fields_ = [
+ ("size", c_uint32),
+ ("event_type", c_int32),
+ ('timestamp', c_double),
+ ("event_data", MCPEventData)
+ ]
+
+MCPBvhRotation = namedtuple('EMCPBvhRotation', [
+ 'XYZ',
+ 'XZY',
+ 'YXZ',
+ 'YZX',
+ 'ZXY',
+ 'ZYX'
+])(0, 1, 2, 3, 4, 5)
+
+MCPBvhData = namedtuple('EMCPBvhData', [
+ 'String',
+ 'BinaryWithOldFrameHeader',
+ 'Binary',
+ 'BinaryLegacyHumanHierarchy'
+])(0, 1, 2, 6)
+
+MCPBvhDisplacement = namedtuple('EMCPBvhDisplacement', [
+ 'Disable',
+ 'Enable'
+])(0, 1)
+
+class MCPTracker(object):
+ IMCPTracker_Version = c_char_p(b"PROC_TABLE:IMCPTracker_001")
+
+ class MCPTrackerApi(Structure):
+ _fields_ = [
+ ('SendMessageData', CFUNCTYPE(c_int32, c_char_p, c_int32, MCPTrackerHandle)),
+ ('GetTrackerRotation', CFUNCTYPE(c_int32, POINTER(c_float),POINTER(c_float),POINTER(c_float),POINTER(c_float), c_char_p, MCPTrackerHandle)),
+ ('GetTrackerPosition', CFUNCTYPE(c_int32, POINTER(c_float),POINTER(c_float),POINTER(c_float), c_char_p, MCPTrackerHandle)),
+ ('GetTrackerEulerAng', CFUNCTYPE(c_int32, POINTER(c_float),POINTER(c_float),POINTER(c_float), c_char_p, MCPTrackerHandle)),
+ ('GetDeviceCount', CFUNCTYPE(c_int32, POINTER(c_int32), MCPTrackerHandle)),
+ ('GetDeviceName', CFUNCTYPE(c_int32, c_int32, POINTER(c_char_p), MCPTrackerHandle)),
+ ]
+
+ api = POINTER(MCPTrackerApi)()
+
+ def __init__(self, tracker_handle):
+ if not self.api:
+ err = MocapApi.MCPGetGenericInterface(self.IMCPTracker_Version, pointer(self.api))
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get MCPTracker interface: {0}'.format(MCPError._fields[err]))
+ self.handle = tracker_handle
+
+ def get_device_name(self, serialNum):
+ name = c_char_p()
+ err = self.api.contents.GetDeviceName(c_int32(serialNum), byref(name), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get device name: {0}'.format(MCPError._fields[err]))
+ return name.value.decode('utf-8')
+
+ def get_device_count(self):
+ devCount = c_int32(0)
+ err = self.api.contents.GetDeviceCount(byref(devCount), self.handle)
+ if err != MCPError.NoError:
+ print(f"GetDeviceCount err: {err}")
+ raise RuntimeError('Can not get device count: {0}'.format(MCPError._fields[err]))
+ return devCount.value
+
+
+ def get_tracker_rotation(self, device_name):
+ x = c_float()
+ y = c_float()
+ z = c_float()
+ w = c_float()
+ device_name_arg = c_char_p(device_name.encode('utf-8'))
+ err = self.api.contents.GetTrackerRotation(byref(x), byref(y), byref(z), byref(w), device_name_arg, self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get tracker rotation:{}'.format(MCPError._fields[err]))
+ return x.value, y.value, z.value, w.value
+
+ def get_tracker_rotataion(self, device_name):
+ return self.get_tracker_rotation(device_name)
+
+ def get_tracker_position(self, device_name):
+ x = c_float()
+ y = c_float()
+ z = c_float()
+ device_name_arg = c_char_p(device_name.encode('utf-8'))
+ err = self.api.contents.GetTrackerPosition(byref(x), byref(y), byref(z), device_name_arg, self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get tracker position:{}'.format(MCPError._fields[err]))
+ return x.value, y.value, z.value
+
+ def send_message_data(self):
+ message = c_char_p()
+ len = c_int32()
+ err = self.api.contents.SendMessageData(message, len, self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not Send Message Data: {0}'.format(MCPError._fields[err]))
+ return message.value, len.value
+
+
+
+class MCPMarker(object):
+ IMCPMarker_Version = c_char_p(b"PROC_TABLE:IMCPMarker_001")
+
+ class MCPMarkerApi(Structure):
+ _fields_ = [
+ ('GetMarkerPosition', CFUNCTYPE(c_int32, POINTER(c_float),POINTER(c_float),POINTER(c_float), MCPMarkerHandle)),
+ ]
+
+ api = POINTER(MCPMarkerApi)()
+
+ def __init__(self, marker_handle):
+ if not self.api:
+ err = MocapApi.MCPGetGenericInterface(self.IMCPMarker_Version, pointer(self.api))
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get MCPMarker interface: {0}'.format(MCPError._fields[err]))
+ self.handle = marker_handle
+
+ def get_marker_position(self):
+ marker_x = c_float()
+ marker_y = c_float()
+ marker_z = c_float()
+ err = self.api.contents.GetMarkerPosition(pointer(marker_x), pointer(marker_y), pointer(marker_z), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get marker position: {0}'.format(MCPError._fields[err]))
+ return marker_x.value, marker_y.value, marker_z.value
+
+MCPSettingsHandle = c_uint64
+
+class MCPSettings(object):
+ IMCPSettingsApi_Version = c_char_p(b'PROC_TABLE:IMCPSettings_001')
+
+ class MCPSettingsApi(Structure):
+ _fields_ = [
+ ('CreateSettings', CFUNCTYPE(c_int32, POINTER(MCPSettingsHandle))),
+ ('DestroySettings', CFUNCTYPE(c_int32, MCPSettingsHandle)),
+ ('SetSettingsUDP', CFUNCTYPE(c_int32, c_uint16, MCPSettingsHandle)),
+ ('SetSettingsTCP', CFUNCTYPE(c_int32, c_char_p, c_uint16, MCPSettingsHandle)),
+ ('SetSettingsBvhRotation', CFUNCTYPE(c_int32, c_int32, MCPSettingsHandle)),
+ ('SetSettingsBvhTransformation', CFUNCTYPE(c_int32, c_int32, MCPSettingsHandle)),
+ ('SetSettingsBvhData', CFUNCTYPE(c_int32, c_int32, MCPSettingsHandle)),
+ ('SetSettingsCalcData', CFUNCTYPE(c_int32, MCPSettingsHandle)),
+ ("SetSettingsUDPServer", CFUNCTYPE(c_int32, c_char_p, c_uint16, MCPSettingsHandle)),
+ ("SetSettingsUDPEx", CFUNCTYPE(c_int32, c_char_p, c_uint16, MCPSettingsHandle))
+ ]
+
+ api = POINTER(MCPSettingsApi)()
+
+ def __init__(self):
+ if not self.api:
+ err = MocapApi.MCPGetGenericInterface(self.IMCPSettingsApi_Version, pointer(self.api))
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get MCPSettings interface: {0}'.format(MCPError._fields[err]))
+ self.handle = MCPSettingsHandle()
+ err = self.api.contents.CreateSettings(pointer(self.handle))
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not create settings: {0}'.format(MCPError._fields[err]))
+
+ def __del__(self):
+ err = self.api.contents.DestroySettings(self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not destroy settings: {0}'.format(MCPError._fields[err]))
+
+ def set_udp(self, local_port):
+ err = self.api.contents.SetSettingsUDP(c_uint16(local_port), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not set udp port of {0}: {1}'.format(local_port, MCPError._fields[err]))
+
+ def set_tcp(self, ip, port):
+ err = self.api.contents.SetSettingsTCP(c_char_p(bytes(ip, encoding='utf8')), c_uint16(port), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not settings tcp addr of {0}:{1}: {2}'.format(ip, port, MCPError._fields[err]))
+
+ def set_bvh_rotation(self, bvh_rotation):
+ err = self.api.contents.SetSettingsBvhRotation(c_int32(bvh_rotation), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not set bvh rotation: {0}'.format(MCPError._fields[err]))
+
+ def set_bvh_displacement(self, bvh_transformation):
+ err = self.api.contents.SetSettingsBvhDisplacement(c_int32(bvh_transformation), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not set bvh displacement: {0}'.format(MCPError._fields[err]))
+ def set_bvh_transformation(self, bvh_transformation):
+ err = self.api.contents.SetSettingsBvhTransformation(c_int32(bvh_transformation), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not set bvh Transformation: {0}'.format(MCPError._fields[err]))
+
+ def set_bvh_data(self, bvh_data):
+ err = self.api.contents.SetSettingsBvhData(c_int32(bvh_data), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not set bvh data: {0}'.format(MCPError._fields[err]))
+
+ def set_calc_data(self):
+ err = self.api.contents.SetSettingsCalcData(self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not set calc data: {0}'.format(MCPError._fields[err]))
+ def SetSettingsUDPServer(self, server_ip, port) -> None:
+ err = self.api.contents.SetSettingsUDPServer(
+ c_char_p(bytes(server_ip, encoding='utf8')),
+ c_uint16(port),
+ self.handle
+ )
+ if err != 0:
+ raise RuntimeError(f"UDPServer config failed with code {err}")
+ def SetSettingsUDPEx(self, local_ip, port) -> None:
+ err = self.api.contents.SetSettingsUDPEx(
+ c_char_p(bytes(local_ip, encoding='utf8')),
+ c_uint16(port),
+ self.handle
+ )
+ if err != 0:
+ raise RuntimeError(f"UDPEx config failed with code {err}")
+
+MCPUpVector = namedtuple('EMCPUpVector', [
+ 'XAxis',
+ 'YAxis',
+ 'ZAxis'
+])(1, 2, 3)
+
+MCPFrontVector = namedtuple('EMCPFrontVector', [
+ 'ParityEven',
+ 'ParityOdd'
+])(1, 2)
+
+MCPCoordSystem = namedtuple('EMCPCoordSystem', [
+ 'RightHanded',
+ 'LeftHanded'
+])(0, 1)
+
+MCPRotatingDirection = namedtuple('EMCPRotatingDirection', [
+ 'Clockwise',
+ 'CounterClockwise'
+])(0, 1)
+
+MCPPreDefinedRenderSettings = namedtuple('EMCPPreDefinedRenderSettings', [
+ 'Default',
+ 'UnrealEngine',
+ 'Unity3D',
+ 'Count'
+])(0, 1, 2, 3)
+
+MCPUnit = namedtuple('EMCPUnit', [
+ 'Centimeter',
+ 'Meter'
+])(0, 1)
+
+MCPRenderSettingsHandle = c_uint64
+
+
+class MCPRenderSettings(object):
+ IMCPRenderSettingsApi_Version = c_char_p(b'PROC_TABLE:IMCPRenderSettings_001')
+
+ class MCPRenderSettingsApi(Structure):
+ _fields_ = [
+ ('CreateRenderSettings', CFUNCTYPE(c_int32, POINTER(MCPRenderSettingsHandle))),
+ ('GetPreDefRenderSettings', CFUNCTYPE(c_int32, c_int32, POINTER(MCPRenderSettingsHandle))),
+ ('SetUpVector', CFUNCTYPE(c_int32, c_int32, c_int32, MCPRenderSettingsHandle)),
+ ('GetUpVector', CFUNCTYPE(c_int32, POINTER(c_int32), POINTER(c_int32), MCPRenderSettingsHandle)),
+ ('SetFrontVector', CFUNCTYPE(c_int32, c_int32, c_int32, MCPRenderSettingsHandle)),
+ ('GetFrontVector', CFUNCTYPE(c_int32, POINTER(c_int32), POINTER(c_int32), MCPRenderSettingsHandle)),
+ ('SetCoordSystem', CFUNCTYPE(c_int32, c_int32, MCPRenderSettingsHandle)),
+ ('GetCoordSystem', CFUNCTYPE(c_int32, POINTER(c_int32), MCPRenderSettingsHandle)),
+ ('SetRotatingDirection', CFUNCTYPE(c_int32, c_int32, MCPRenderSettingsHandle)),
+ ('GetRotationDirection', CFUNCTYPE(c_int32, POINTER(c_int32), MCPRenderSettingsHandle)),
+ ('SetUnit', CFUNCTYPE(c_int32, c_int32, MCPRenderSettingsHandle)),
+ ('GetUnit', CFUNCTYPE(c_int32, POINTER(c_int32), MCPRenderSettingsHandle)),
+ ('DestroyRenderSettings', CFUNCTYPE(c_int32, MCPRenderSettingsHandle))
+ ]
+
+ api = POINTER(MCPRenderSettingsApi)()
+
+ def __init__(self, pre_def=None):
+ if not self.api:
+ err = MocapApi.MCPGetGenericInterface(self.IMCPRenderSettingsApi_Version, pointer(self.api))
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get MCPRenderSettings interface: {0}'.format(MCPError._fields[err]))
+ self.pre_def = pre_def
+ self.handle = MCPRenderSettingsHandle()
+ if self.pre_def == None:
+ err = self.api.contents.CreateRenderSettings(pointer(self.handle))
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not create render settings: {0}'.format(MCPError._fields[err]))
+ else:
+ err = self.api.contents.GetPreDefRenderSettings(c_int32(pre_def), pointer(self.handle))
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get render settings: {0}'.format(MCPError._fields[err]))
+
+ def __del__(self):
+ if self.pre_def == None:
+ err = self.api.contents.DestroyRenderSettings(self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not destroy render settings: {0}'.format(MCPError._fields[err]))
+
+ def set_up_vector(self, up_vector, sign):
+ err = self.api.contents.SetUpVector(c_int32(up_vector), c_int32(sign), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not set up vector: {0}'.format(MCPError._fields[err]))
+
+ def get_up_vector(self):
+ up_vector = c_int32()
+ sign = c_int32()
+ err = self.api.contents.GetUpVector(pointer(up_vector), pointer(sign), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get up vector: {0}'.format(MCPError._fields[err]))
+ return up_vector.value, sign.value
+
+ def set_front_vector(self, front_vector, sign):
+ err = self.api.contents.SetFrontVector(c_int32(front_vector), c_int32(sign), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not set front vector: {0}'.format(MCPError._fields[err]))
+
+ def get_front_vector(self):
+ front_vector = c_int32()
+ sign = c_int32()
+ err = self.api.contents.GetFrontVector(pointer(front_vector), pointer(sign), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get front vector: {0}'.format(MCPError._fields[err]))
+ return front_vector.value, sign.value
+
+ def set_coord_system(self, coord_sys):
+ err = self.api.contents.SetCoordSystem(c_int32(coord_sys), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not set coord system: {0}'.format(MCPError._fields[err]))
+
+ def get_coord_system(self):
+ coord_sys = c_int32()
+ err = self.api.contents.GetCoordSystem(pointer(coord_sys), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get coord system: {0}'.format(MCPError._fields[err]))
+ return coord_sys.value
+
+ def set_rotating_direction(self, rotating_direction):
+ err = self.api.contents.SetRotatingDirection(c_int32(rotating_direction), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not set rotating direction: {0}'.format(MCPError._fields[err]))
+
+ def get_rotating_direction(self):
+ rotating_direction = c_int32()
+ err = self.api.contents.GetRotatingDirection(pointer(rotating_direction), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get rotating direction: {0}'.format(MCPError._fields[err]))
+ return rotating_direction.value
+
+ def set_unit(self, unit):
+ err = self.api.contents.SetUnit(c_int32(unit), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not set unit: {0}'.format(MCPError._fields[err]))
+
+ def get_unit(self):
+ unit = c_int32()
+ err = self.api.contents.GetUnit(pointer(unit), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get unit: {0}'.format(MCPError._fields[err]))
+ return unit.value
+
+MCPRobotJointType=namedtuple('EMCPRobotJointType',
+ ['JointLeftShoulder_Pitch',
+ 'JointLeftShoulder_Roll',
+ 'JointLeftShoulder_Yaw',
+ 'JointLeftElbow',
+ 'JointLeftWrist_Yaw',
+ 'JointLeftWrist_Roll',
+ 'JointLeftWrist_Pitch',
+ 'JointRightShoulder_Pitch',
+ 'JointRightShoulder_Roll',
+ 'JointRightShoulder_Yaw',
+ 'JointRightElbow',
+ 'JointRightWrist_Yaw',
+ 'JointRightWrist_Roll',
+ 'JointRightWrist_Pitch',
+ 'JointLeftHip_Yaw',
+ 'JointLeftHip_Pitch',
+ 'JointLeftHip_Roll',
+ 'JointLeftKnee',
+ 'JointLeftAnkle_Pitch',
+ 'JointLeftAnkle_Roll',
+ 'JointRightHip_Yaw',
+ 'JointRightHip_Pitch',
+ 'JointRightHip_Roll',
+ 'JointRightKnee',
+ 'JointRightAnkle_Pitch',
+ 'JointRightAnkle_Roll',
+ 'JointSpine_Yaw',
+ 'JointSpine_Roll',
+ 'JointSpine_Pitch',
+ 'JointHead_Yaw',
+ 'JointHead_Roll',
+ 'JointHead_Pitch',
+ 'JointCount'])._make([x for x in range(0,33)])
+
+MCPRobotHandle = c_uint64
+
+class MCPRobot(object):
+ IMCPRobot_Version = c_char_p(b"PROC_TABLE:IMCPRobot_001")
+ class MCPRobotApi(Structure):
+ _fields_=[
+ ('CreateRobot', CFUNCTYPE(c_int32,c_char_p,POINTER(MCPRobotHandle))),
+ ('SetRobotFPS', CFUNCTYPE(c_int32, c_int32, MCPRobotHandle)),
+ ('UpdateRobot', CFUNCTYPE(c_int32, MCPAvatarHandle, MCPRobotHandle)),
+ ('GetRobotRawJointAngle', CFUNCTYPE(c_int32, c_int32, POINTER(c_float), MCPRobotHandle)),
+ ('GetRobotRetargetJointAngle', CFUNCTYPE(c_int32, c_char_p, POINTER(c_float), MCPRobotHandle)),
+ ('GetRobotRosFrameJson', CFUNCTYPE(c_int32, POINTER(c_char_p), c_bool, MCPRobotHandle)),
+ ('GetRobotRootRotation', CFUNCTYPE(c_int32, POINTER(c_float), POINTER(c_float), POINTER(c_float), POINTER(c_float), MCPRobotHandle)),
+ ('GetRobotRootPosition', CFUNCTYPE(c_int32, POINTER(c_float), POINTER(c_float), POINTER(c_float), MCPRobotHandle)),
+ ('DestroyRobot', CFUNCTYPE(c_int32, MCPRobotHandle)),
+ ('RunRobotStep', CFUNCTYPE(c_int32, MCPRobotHandle))
+ ]
+ api = POINTER(MCPRobotApi)()
+ def __init__(self, json_robot_desc):
+ if not self.api:
+ err = MocapApi.MCPGetGenericInterface(self.IMCPRobot_Version, pointer(self.api))
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get MCPRobot interface: {}'.format(MCPError._fields[err]))
+ self.handle = MCPSettingsHandle()
+ err = self.api.contents.CreateRobot(c_char_p(bytes(json_robot_desc, encoding='utf8')), pointer(self.handle))
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not create robot: {0}'.format(MCPError._fields[err]))
+ def __del__(self):
+ err = self.api.contents.DestroyRobot(self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not destroy robot : {0}'.format(MCPError._fields[err]))
+
+ def set_robot_fps(self,fps):
+ err = self.api.contents.SetRobotFPS(c_uint32(fps),self.handle)
+ if self != MCPError.NoError:
+ raise RuntimeError('Can not set robot fps: {0}'.format(MCPError._fields[err]))
+ def update_robot(self,avatar):
+ err = self.api.contents.UpdateRobot(avatar.handle, self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not update robot: {0}'.format(MCPError._fields[err]))
+ def get_robot_raw_joint_angle(self,robotJointType):
+ val = c_float()
+ err = self.api.contents.GetRobotRawJointAngle(c_int32(robotJointType),pointer(val), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get robot raw joint angle: {0}'.format(MCPError._fields[err]))
+ return val.value
+ def get_robot_retarget_joint_angle(self,jointName):
+ val = c_float()
+ err = self.api.contents.GetRobotRetargetJointAngle(c_char_p(bytes(jointName,encoding='utf8')), pointer(val),self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get robot retarget joint angle: {0}'.format(MCPError._fields[err]))
+ return val.value
+ def get_robot_ros_frame_json(self):
+ val = c_float()
+ json_str = c_char_p()
+ err = self.api.contents.GetRobotRosFrameJson(pointer(json_str), pointer(val), self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get robot ros frame json: {0}'.format(MCPError._fields[err]))
+ return str(json_str.value, encoding='utf8'),val.value
+ def get_robot_root_rotataion(self):
+ x = c_float()
+ y = c_float()
+ z = c_float()
+ w = c_float()
+ err = self.api.contents.GetRobotRootRotation(pointer(x),pointer(y),pointer(z),pointer(w),self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get robot robot rotation:{}'.format(MCPError._fields[err]))
+ return x.value, y.value, z.value, w.value
+ def get_robot_root_position(self):
+ x = c_float()
+ y = c_float()
+ z = c_float()
+ err = self.api.contents.GetRobotRootPosition(pointer(x), pointer(y), pointer(z),self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get robot root position:{}'.format(MCPError._fields[err]))
+ return x.value, y.value, z.value
+ def run_robot_step(self):
+ err = self.api.contents.RunRobotStep(self.handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not run robot step:{}'.format(MCPError._fields[err]))
+
+EMCPCommand = namedtuple('EMCPCommand', [
+ 'CommandStartCapture',
+ 'CommandStopCapture',
+ 'CommandZeroPosition',
+ 'CommandCalibrateMotion',
+ 'CommandStartRecored',
+ 'CommandStopRecored',
+ 'CommandResumeOriginalPosture',
+ 'CommandClearZeroMotionDrift',
+ "CommandResumeOriginalHandsPosture"
+])(0, 1, 2, 3,4,5,6,7,8)
+
+MCPApplicationHandle = c_uint64
+MCPTrackerHandle = c_uint64
+MCPEventHandleProc = c_uint64
+class MCPApplication(object):
+ IMCPApplicationApi_Version = c_char_p(b'PROC_TABLE:IMCPApplication_002')
+
+ class MCPApplicationApi(Structure):
+ _fields_ = [
+ ('CreateApplication', CFUNCTYPE(c_int32, POINTER(MCPApplicationHandle))),
+ ('DestroyApplication', CFUNCTYPE(c_int32, MCPApplicationHandle)),
+ ('SetApplicationSettings', CFUNCTYPE(c_int32, MCPSettingsHandle, MCPApplicationHandle)),
+ ('SetApplicationRenderSettings', CFUNCTYPE(c_int32, MCPRenderSettingsHandle, MCPApplicationHandle)),
+ ('OpenApplication', CFUNCTYPE(c_int32, MCPApplicationHandle)),
+ ('EnableApplicationCacheEvents', CFUNCTYPE(c_int32, MCPApplicationHandle)),
+ ('DisableApplicationCacheEvents', CFUNCTYPE(c_int32, MCPApplicationHandle)),
+ ('ApplicationCacheEventsIsEnabled', CFUNCTYPE(c_int32, POINTER(c_bool), MCPApplicationHandle)),
+ ('CloseApplication', CFUNCTYPE(c_int32, MCPApplicationHandle)),
+ ('GetApplicationRigidBodies', CFUNCTYPE(c_int32, POINTER(c_uint64), POINTER(c_uint32), MCPApplicationHandle)),
+ ('GetApplicationAvatars', CFUNCTYPE(c_int32, POINTER(c_uint64), POINTER(c_uint32), MCPApplicationHandle)),
+ ('PollApplicationNextEvent', CFUNCTYPE(c_int32, POINTER(MCPEvent), POINTER(c_uint32), MCPApplicationHandle)),
+ ('GetApplicationSensorModules', CFUNCTYPE(c_int32, POINTER(MCPSensorModuleHandle), POINTER(c_uint32), MCPApplicationHandle)),
+ ('GetApplicationTrackers', CFUNCTYPE(c_int32, POINTER(MCPTrackerHandle), POINTER(c_uint32), MCPApplicationHandle)),
+ ('QueuedServerCommand', CFUNCTYPE(c_int32, MCPCommandHandle, MCPApplicationHandle)),
+ ('RegisterEventHandler', CFUNCTYPE(c_int32, MCPEventHandleProc, POINTER(c_uint64), MCPApplicationHandle)),
+ ('UnregisterEventHandler', CFUNCTYPE(c_int32, MCPEventHandleProc, POINTER(c_uint64), MCPApplicationHandle)),
+ ('GetAppplicationSystem', CFUNCTYPE(c_int32, POINTER(MCPSystemHandle), MCPApplicationHandle)),
+ ]
+
+ api = POINTER(MCPApplicationApi)()
+
+ def __init__(self):
+ if not self.api:
+ err = MocapApi.MCPGetGenericInterface(self.IMCPApplicationApi_Version, pointer(self.api))
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get IMCPApplication interface: {0}'.format(MCPError._fields[err]))
+ self._handle = MCPApplicationHandle()
+ err = self.api.contents.CreateApplication(pointer(self._handle))
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not create application: {0}'.format(MCPError._fields[err]))
+ self._is_opened = False
+
+ def queue_command(self, cmd_type):
+ cmd = MCPCommand()
+ cmd.create_command(cmd_type)
+ err = self.api.contents.QueuedServerCommand(cmd.handle, self._handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not queued server command: {0}'.format(MCPError._fields[err]))
+
+ def queue_command_with_extra_long(self, cmd_type, extra_long):
+ cmd = MCPCommand()
+ cmd.create_command(cmd_type) # 先创建命令初始化句柄
+ # 根据C++实现,使用CommandExtraLong_Extra0(值为0)作为commandType
+ cmd.set_command_extra_long(extra_long) # 0 is CommandExtraLong_Extra0
+ err = self.api.contents.QueuedServerCommand(cmd.handle, self._handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not queued server command: {0}'.format(MCPError._fields[err]))
+
+ def __del__(self):
+ err = self.api.contents.DestroyApplication(self._handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not destroy application: {0}'.format(MCPError._fields[err]))
+
+ def set_settings(self, settings):
+ err = self.api.contents.SetApplicationSettings(settings.handle, self._handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not set application settings: {0}'.format(MCPError._fields[err]))
+
+ def set_render_settings(self, settings):
+ err = self.api.contents.SetApplicationRenderSettings(settings.handle, self._handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not set application render settings: {0}'.format(MCPError._fields[err]))
+
+ def open(self):
+ err = self.api.contents.OpenApplication(self._handle)
+ self._is_opened = (err == MCPError.NoError)
+ return err == MCPError.NoError, MCPError._fields[err]
+
+ def is_opened(self):
+ return self._is_opened
+
+ def enable_event_cache(self):
+ err = self.api.contents.EnableApplicationCacheEvents(self._handle)
+ return err == MCPError.NoError, MCPError._fields[err]
+
+ def disable_event_cache(self):
+ err = self.api.contents.DisableApplicationCacheEvents(self._handle)
+ return err == MCPError.NoError, MCPError._fields[err]
+
+ def is_event_cache_enabled(self):
+ enable = c_bool()
+ err = self.api.contents.ApplicationCacheEventsIsEnabled(pointer(enable), self._handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get application event cache settings: {0}'.format(MCPError._fields[err]))
+ return enable.value
+
+ def close(self):
+ err = self.api.contents.CloseApplication(self._handle)
+ self._is_opened = False
+ return err == MCPError.NoError, MCPError._fields[err]
+
+ def get_rigid_bodies(self):
+ rigid_body_size = c_uint32()
+ err = self.api.contents.GetApplicationRigidBodies(POINTER(MCPRigidBodyHandle)(), pointer(rigid_body_size),
+ self._handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get application rigid bodies: {0}'.format(MCPError._fields[err]))
+ rigid_body_handles = (MCPRigidBodyHandle * rigid_body_size.value)()
+ err = self.api.GetApplicationRigidBodies(rigid_body_handles, pointer(rigid_body_size), self._handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get application rigid bodies: {0}'.format(MCPError._fields[err]))
+ return [MCPRigidBody(rigid_body_handles[i]) for i in range(rigid_body_size.value)]
+
+ def get_avatars(self):
+ avatar_count = c_uint32()
+ err = self.api.contents.GetApplicationAvatars(POINTER(MCPAvatarHandle)(), pointer(avatar_count), self._handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get application avatars: {0}'.format(MCPError._fields[err]))
+ avatar_handles = (MCPAvatarHandle * avatar_count.value)()
+ err = self.api.contents.GetApplicationAvatars(avatar_handles, pointer(avatar_count), self._handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get application avatars: {0}'.format(MCPError._fields[err]))
+ return [MCPAvatar(avatar_handles[i]) for i in range(avatar_count.value)]
+ def get_trackers(self):
+ tracker_count = c_uint32()
+ err = self.api.contents.GetApplicationTrackers(POINTER(MCPTrackerHandle)(), pointer(tracker_count), self._handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get application trackers: {0}'.format(MCPError._fields[err]))
+ tracker_handles = (MCPTrackerHandle * tracker_count.value)()
+ err = self.api.contents.GetApplicationTrackers(tracker_handles, pointer(tracker_count), self._handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get application trackers: {0}'.format(MCPError._fields[err]))
+ return [MCPTracker(tracker_handles[i]) for i in range(tracker_count.value)]
+
+ def poll_next_event(self):
+ evt_count = c_uint32(0)
+ err = self.api.contents.PollApplicationNextEvent(POINTER(MCPEvent)(), pointer(evt_count), self._handle)
+ if err not in (MCPError.NoError, MCPError.MoreEvent):
+ raise RuntimeError('Can not poll application next event: {0}'.format(MCPError._fields[err]))
+ if evt_count.value == 0:
+ return []
+ evt_array = (MCPEvent * evt_count.value)()
+ for i in range(evt_count.value):
+ evt_array[i].size = sizeof(MCPEvent)
+ err = self.api.contents.PollApplicationNextEvent(evt_array, pointer(evt_count), self._handle)
+ if err not in (MCPError.NoError, MCPError.MoreEvent):
+ raise RuntimeError('Can not poll application next event: {0}'.format(MCPError._fields[err]))
+ if evt_count.value == 0:
+ return []
+ return [evt_array[i] for i in range(evt_count.value)]
+
+ def get_system(self):
+ systemHandle = MCPSystemHandle()
+ err = self.api.contents.GetAppplicationSystem(pointer(systemHandle), self._handle)
+ if err != MCPError.NoError:
+ raise RuntimeError('Can not get application system: {0}'.format(MCPError._fields[err]))
+ return systemHandle
+
+def test_mocap_api():
+ def print_joint(joint):
+ print(joint.get_name())
+ print(joint.get_local_rotation())
+ print(joint.get_local_rotation_by_euler())
+ print(joint.get_local_position())
+
+ children = joint.get_children()
+ for child in children:
+ print_joint(child)
+
+ app = MCPApplication()
+ settings = MCPSettings()
+ settings.set_udp(7012)
+ app.set_settings(settings)
+ app.open()
+ # app.queue_command1(1)
+
+
+ while True:
+ evts = app.poll_next_event()
+ for evt in evts:
+ if evt.event_type == MCPEventType.AvatarUpdated:
+ avatar = MCPAvatar(evt.event_data.avatar_handle)
+ print(f"get_avatar_posture_ptp_time():{avatar.get_avatar_posture_ptp_time()}")
+ elif evt.event_type == MCPEventType.RigidBodyUpdated:
+ print('rigid body updated')
+ elif evt.event_type == MCPEventType.AliceTrackerUpdated:
+ _handle_tracker_data()
+ elif evt.event_type == MCPEventType.AliceMarkerUpdated:
+ _handle_marker_data()
+ else:
+ print('unknow event')
+ time.sleep(0.001)
+
+def _handle_marker_data():
+ alicehub = MCPAliceHub()
+ recv, count = alicehub.get_marker_list()
+ if count > 0:
+ recv, count1 = alicehub.get_marker_list(count)
+ for i in range(count):
+ marker_handle = recv[i]
+ marker = MCPMarker(marker_handle)
+ position = marker.get_marker_position()
+ print('position:', position)
+
+
+def _handle_tracker_data():
+ alicehub = MCPAliceHub()
+ recv, count = alicehub.get_PWR_list()
+ if count > 0:
+ recv, count1 = alicehub.get_PWR_list(count)
+ timestamp = alicehub.get_PWR_timestamp()
+ for i in range(count):
+ PWRHandle = recv[i]
+ MCPPWRH = MCPPWR(PWRHandle)
+ id = MCPPWRH.get_PWR_id()
+ status = MCPPWRH.get_PWR_status()
+ position = MCPPWRH.get_PWR_position()
+ quaternion = MCPPWRH.get_PWR_quaternion()
+ print('timestamp',timestamp,'id:', id, 'status:', status, 'position:', position, 'quaternion:', quaternion)
+if __name__ == '__main__':
+ test_mocap_api()
\ No newline at end of file
diff --git a/src/plugins/noitom_mocap/tools/noitom_mocap_printer.py b/src/plugins/noitom_mocap/tools/noitom_mocap_printer.py
new file mode 100755
index 000000000..be8cc2522
--- /dev/null
+++ b/src/plugins/noitom_mocap/tools/noitom_mocap_printer.py
@@ -0,0 +1,149 @@
+#!/usr/bin/env python3
+# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+"""Print live full-body frames published by noitom_mocap_plugin."""
+
+from __future__ import annotations
+
+import argparse
+import time
+
+import isaacteleop.deviceio as deviceio
+import isaacteleop.oxr as oxr
+from isaacteleop.schema import BodyJointPN
+
+
+_JOINT_NAMES = (
+ "pelvis",
+ "left_hip",
+ "right_hip",
+ "spine1",
+ "left_knee",
+ "right_knee",
+ "spine2",
+ "left_ankle",
+ "right_ankle",
+ "spine3",
+ "left_foot",
+ "right_foot",
+ "neck",
+ "left_collar",
+ "right_collar",
+ "head",
+ "left_shoulder",
+ "right_shoulder",
+ "left_elbow",
+ "right_elbow",
+ "left_wrist",
+ "right_wrist",
+ "left_hand",
+ "right_hand",
+)
+
+
+def _format_point(point: object | None) -> str:
+ if point is None:
+ return "[n/a]"
+ return f"[{point.x:+.3f}, {point.y:+.3f}, {point.z:+.3f}]"
+
+
+def _joint(frame: object, index: int) -> object:
+ return frame.joints.joints(index)
+
+
+def _print_frame(
+ frame: object,
+ frame_index: int,
+ elapsed_s: float,
+ max_joints: int,
+) -> None:
+ joint_count = int(BodyJointPN.NUM_JOINTS)
+ valid_joints = (
+ 0
+ if frame.joints is None
+ else sum(1 for index in range(joint_count) if _joint(frame, index).is_valid)
+ )
+
+ print(
+ f"[{elapsed_s:6.2f}s] frame={frame_index:05d} "
+ f"joints={valid_joints}/{joint_count} "
+ f"all_joint_poses_tracked={frame.all_joint_poses_tracked}"
+ )
+
+ if frame.joints is None:
+ return
+ for index in range(min(max_joints, joint_count)):
+ joint = _joint(frame, index)
+ print(
+ f" joint {_JOINT_NAMES[index]:16s} valid={joint.is_valid} "
+ f"pos={_format_point(joint.pose.position)}"
+ )
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument(
+ "--collection-id", default="noitom_mocap", help="OpenXR tensor collection id."
+ )
+ parser.add_argument("--max-flatbuffer-size", type=int, default=16 * 1024)
+ parser.add_argument(
+ "--duration",
+ type=float,
+ default=10.0,
+ help="Seconds to read. Use 0 for forever.",
+ )
+ parser.add_argument("--rate", type=float, default=60.0, help="Polling rate in Hz.")
+ parser.add_argument(
+ "--print-period", type=float, default=0.5, help="Seconds between status prints."
+ )
+ parser.add_argument("--max-joints", type=int, default=6)
+ args = parser.parse_args()
+
+ tracker = deviceio.FullBodyTrackerPN(args.collection_id, args.max_flatbuffer_size)
+ required_extensions = deviceio.DeviceIOSession.get_required_extensions([tracker])
+
+ print("Noitom full-body printer")
+ print(f" collection_id={args.collection_id}")
+ print(" positions=meters")
+ print(f" required_extensions={required_extensions}")
+ print(" start noitom_mocap_plugin before running this script")
+
+ sleep_s = 0.0 if args.rate <= 0.0 else 1.0 / args.rate
+ start_s = time.monotonic()
+ last_print_s = 0.0
+ frame_index = 0
+ seen_frame = False
+
+ with oxr.OpenXRSession("NoitomMocapPrinter", required_extensions) as oxr_session:
+ handles = oxr_session.get_handles()
+ with deviceio.DeviceIOSession.run([tracker], handles) as session:
+ while args.duration <= 0.0 or time.monotonic() - start_s < args.duration:
+ session.update()
+ tracked = tracker.get_body_pose(session)
+ frame = tracked.data
+ elapsed_s = time.monotonic() - start_s
+
+ if frame is None:
+ if elapsed_s - last_print_s >= args.print_period:
+ print(f"[{elapsed_s:6.2f}s] no Noitom frame yet")
+ last_print_s = elapsed_s
+ elif elapsed_s - last_print_s >= args.print_period:
+ seen_frame = True
+ _print_frame(frame, frame_index, elapsed_s, args.max_joints)
+ last_print_s = elapsed_s
+
+ frame_index += 1
+ if sleep_s > 0.0:
+ time.sleep(sleep_s)
+
+ if not seen_frame:
+ print(
+ "No frames received. Check that the plugin is running and the collection id matches."
+ )
+ return 1
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())