Repository navigation
Add tactile schemas, trackers, bindings and sources #1242
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
lotusl-code
wants to merge
8
commits into
main
Choose a base branch
from
lotusl/add-tactile-schema
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+2,948
−18
Open
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
6dd98ba
Add tactile schemas, trackers, bindings and sources
lotusl-code 6605bd1
Address review comments on the tactile schemas
lotusl-code a85f9b3
Publish TactileShape as tactile tracker metadata
lotusl-code 9802039
Push tracker metadata in chunks across frames
lotusl-code 7d4baa6
Add a tactile feeder and printer example
lotusl-code 16ab20a
Remove the unused scalar_vector_view helper
lotusl-code e51aaef
Drop the unused record_type manifest key
lotusl-code 3fa340c
Drop SchemaMetadataPusher's duplicate chunk-size check
lotusl-code File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| cmake_minimum_required(VERSION 3.20) | ||
|
|
||
| include(${CMAKE_SOURCE_DIR}/cmake/InstallPythonExample.cmake) | ||
| install_python_example(DESTINATION examples/tactile) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| <!-- | ||
| SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| SPDX-License-Identifier: Apache-2.0 | ||
| --> | ||
|
|
||
| # Tactile | ||
|
|
||
| A mock tactile device and a consumer for exercising the tactile tracker without hardware. | ||
|
|
||
| - `feeder` publishes a 26-joint hand with 200 taxels by default. Each frame carries one | ||
| `TactileData` and the next chunk of the `TactileShape` metadata. It publishes through | ||
| `TensorPushTracker` on a `DeviceIOSession`, as a tactile plugin would through `SchemaPusher` | ||
| and `SchemaMetadataPusher`. | ||
| - `printer` reads one `TactileTracker` directly. It discovers the joint and taxel counts from | ||
| `TactileShape`, prints the first and last taxel forces, and records the session to MCAP. | ||
|
|
||
| ```text | ||
| feeder -> "tactile" (TactileData, every frame) | ||
| -> "tactile/metadata" (TactileShape chunks, in a loop) | ||
| -> TactileTracker -> printer, and tactile/* MCAP channels | ||
| ``` | ||
|
|
||
| Install the example: | ||
|
|
||
| ```bash | ||
| uv pip install -e ./examples/tactile | ||
| ``` | ||
|
|
||
| Start the printer first; it starts the CloudXR runtime, and no headset needs to connect: | ||
|
|
||
| ```bash | ||
| python -m isaaccapture_examples.tactile.printer --accept-eula --duration 0 | ||
| ``` | ||
|
|
||
| Then, in another terminal, start the feeder. It attaches to the same runtime: | ||
|
|
||
| ```bash | ||
| python -m isaaccapture_examples.tactile.feeder | ||
| ``` | ||
|
|
||
| Stop the printer with `Ctrl+C`; it writes `./recordings/tactile_<timestamp>.mcap`. Print a | ||
| recording with no runtime at all: | ||
|
|
||
| ```bash | ||
| python -m isaaccapture_examples.tactile.printer --replay recordings/tactile_<timestamp>.mcap | ||
| ``` | ||
|
|
||
| Use feeder options `--joints` and `--taxels` to change the shape; the printer discovers both | ||
| counts from metadata. The two processes need only agree on `--collection-id`. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| [build-system] | ||
| requires = ["hatchling"] | ||
| build-backend = "hatchling.build" | ||
|
|
||
| [project] | ||
| name = "isaaccapture-examples-tactile" | ||
| version = "0.0.0" | ||
| description = "Isaac Teleop tactile feeder and printer" | ||
| requires-python = ">=3.11,<3.14" | ||
| dependencies = [ | ||
| "isaaccapture[cloudxr]", | ||
| "numpy>=1.23.0", | ||
| ] | ||
|
|
||
| [tool.hatch.build.targets.wheel] | ||
| only-include = ["python/isaaccapture_examples/tactile"] | ||
| sources = ["python"] | ||
|
|
||
| [tool.uv.sources] | ||
| isaaccapture = { path = "../..", editable = true } |
2 changes: 2 additions & 0 deletions
2
examples/tactile/python/isaaccapture_examples/tactile/__init__.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 |
14 changes: 14 additions & 0 deletions
14
examples/tactile/python/isaaccapture_examples/tactile/common.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| """Values the feeder and the printer must agree on.""" | ||
|
|
||
| DEFAULT_COLLECTION_ID = "tactile" | ||
|
|
||
| MIN_NUM_JOINTS = 2 | ||
| MAX_NUM_JOINTS = 26 | ||
| DEFAULT_NUM_JOINTS = MAX_NUM_JOINTS | ||
| DEFAULT_NUM_TAXELS = 200 | ||
|
|
||
| # TactileTracker's default max_flatbuffer_size: the reader rejects a larger collection. | ||
| TACTILE_SAMPLE_SIZE = 16384 |
197 changes: 197 additions & 0 deletions
197
examples/tactile/python/isaaccapture_examples/tactile/feeder.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,197 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| """Publish mock tactile frames and their TactileShape metadata, as a tactile device would. | ||
|
|
||
| Each frame pushes one TactileData sample on the data collection and the next chunk(s) of the | ||
| TactileShape on "<collection_id>/metadata", looping over the chunks for as long as it runs. | ||
| Pair it with ``python -m isaaccapture_examples.tactile.printer``. | ||
| """ | ||
|
|
||
| import argparse | ||
| import logging | ||
| import math | ||
| import sys | ||
| import time | ||
| from contextlib import ExitStack | ||
|
|
||
| import numpy as np | ||
|
|
||
| from isaaccapture.cloudxr import CloudXRLauncher | ||
| from isaaccapture.deviceio_session import DeviceIOSession | ||
| from isaaccapture.deviceio_trackers import TensorPushTracker | ||
| from isaaccapture.oxr import OpenXRSession | ||
| from isaaccapture.schema import ( | ||
| TRACKER_METADATA_CHUNK_SIZE, | ||
| TRACKER_METADATA_TENSOR_IDENTIFIER, | ||
| TactileData, | ||
| TactileShape, | ||
| split_tracker_metadata, | ||
| tracker_metadata_collection_id, | ||
| ) | ||
|
|
||
| from .common import ( | ||
| DEFAULT_COLLECTION_ID, | ||
| DEFAULT_NUM_JOINTS, | ||
| DEFAULT_NUM_TAXELS, | ||
| MAX_NUM_JOINTS, | ||
| MIN_NUM_JOINTS, | ||
| TACTILE_SAMPLE_SIZE, | ||
| ) | ||
|
|
||
| logger = logging.getLogger("isaaccapture.examples.tactile.feeder") | ||
|
|
||
| WRIST = 1 | ||
| # First and distal joint of each finger, thumb to little, as XrHandJointEXT ordinals. | ||
| FINGER_FIRST_JOINTS = (2, 6, 11, 16, 21) | ||
| FINGER_DISTAL_JOINTS = (4, 9, 14, 19, 24) | ||
|
|
||
|
|
||
| def mock_shape(num_joints: int, num_taxels: int) -> TactileShape: | ||
| """Build a flat hand in hand-root space with taxels on its distal segments.""" | ||
| positions = np.zeros((num_joints, 3), np.float32) | ||
| parents = np.full(num_joints, WRIST, np.uint32) | ||
| parents[WRIST] = TactileShape.ROOT_PARENT | ||
| positions[0] = (0.0, 0.05, 0.0) | ||
|
|
||
| taxel_joints = [] | ||
| for finger, first in enumerate(FINGER_FIRST_JOINTS): | ||
| if first >= num_joints: | ||
| break | ||
| finger_end = ( | ||
| FINGER_FIRST_JOINTS[finger + 1] - 1 if finger < 4 else MAX_NUM_JOINTS - 1 | ||
| ) | ||
| last = min(finger_end, num_joints - 1) | ||
| for joint in range(first, last + 1): | ||
| positions[joint] = ( | ||
| -0.04 + 0.02 * finger, | ||
| 0.02 + 0.025 * (joint - first), | ||
| 0.0, | ||
| ) | ||
| if joint > first: | ||
| parents[joint] = joint - 1 | ||
| taxel_joints.append(min(FINGER_DISTAL_JOINTS[finger], last)) | ||
|
|
||
| if not taxel_joints: | ||
| taxel_joints.append(WRIST) | ||
|
|
||
| taxel_positions = np.zeros((num_taxels, 3), np.float32) | ||
| joint_indices = np.zeros((num_taxels, 4), np.uint32) | ||
| joint_weights = np.zeros((num_taxels, 4), np.float32) | ||
| per_segment = math.ceil(num_taxels / len(taxel_joints)) | ||
| for taxel in range(num_taxels): | ||
| segment, slot = taxel % len(taxel_joints), taxel // len(taxel_joints) | ||
| joint = taxel_joints[segment] | ||
| taxel_positions[taxel] = positions[joint] + ( | ||
| 0.0, | ||
| 0.02 * slot / per_segment, | ||
| -0.005, | ||
| ) | ||
| joint_indices[taxel] = joint | ||
| joint_weights[taxel, 0] = 1.0 | ||
|
|
||
| return TactileShape( | ||
| joint_bind_positions=positions, | ||
| joint_bind_orientations=np.tile([0.0, 0.0, 0.0, 1.0], (num_joints, 1)), | ||
| joint_parents=parents, | ||
| taxel_positions=taxel_positions, | ||
| taxel_normals=np.tile([0.0, 0.0, -1.0], (num_taxels, 1)), | ||
| taxel_tangents=np.tile([1.0, 0.0, 0.0], (num_taxels, 1)), | ||
| taxel_joint_indices=joint_indices, | ||
| taxel_joint_weights=joint_weights, | ||
| ) | ||
|
|
||
|
|
||
| def mock_forces(num_taxels: int, t: float) -> np.ndarray: | ||
| """Generate a positive 0.5 Hz Z-force wave, phase-shifted per finger.""" | ||
| finger = np.arange(num_taxels) % 5 | ||
| phase = 2.0 * math.pi * 0.5 * t - 1.2 * finger | ||
| forces = np.zeros((num_taxels, 3), np.float32) | ||
| forces[:, 2] = 1.25 + 0.75 * np.sin(phase) | ||
| return forces | ||
|
|
||
|
|
||
| def main(argv: list[str]) -> int: | ||
| parser = argparse.ArgumentParser(description=__doc__) | ||
| parser.add_argument("--collection-id", default=DEFAULT_COLLECTION_ID) | ||
| parser.add_argument("--joints", type=int, default=DEFAULT_NUM_JOINTS) | ||
| parser.add_argument("--taxels", type=int, default=DEFAULT_NUM_TAXELS) | ||
| parser.add_argument("--rate", type=float, default=60.0, help="Frames per second") | ||
| parser.add_argument( | ||
| "--chunks-per-frame", | ||
| type=int, | ||
| default=1, | ||
| help="TactileShape chunks pushed with each frame", | ||
| ) | ||
| parser.add_argument( | ||
| "--duration", | ||
| type=float, | ||
| default=0.0, | ||
| help="Seconds to run; 0 runs until Ctrl+C", | ||
| ) | ||
| CloudXRLauncher.add_launcher_arguments(parser) | ||
| args = parser.parse_args(argv[1:]) | ||
| if not MIN_NUM_JOINTS <= args.joints <= MAX_NUM_JOINTS: | ||
| parser.error(f"--joints must be between {MIN_NUM_JOINTS} and {MAX_NUM_JOINTS}") | ||
| if args.taxels <= 0 or args.chunks_per_frame <= 0: | ||
| parser.error("--taxels and --chunks-per-frame must be positive") | ||
| if not math.isfinite(args.rate) or args.rate <= 0: | ||
| parser.error("--rate must be finite and positive") | ||
|
|
||
| chunks = split_tracker_metadata(mock_shape(args.joints, args.taxels).to_bytes()) | ||
| logger.info( | ||
| "Publishing %d joints and %d taxels on '%s'; TactileShape in %d chunk(s)", | ||
| args.joints, | ||
| args.taxels, | ||
| args.collection_id, | ||
| len(chunks), | ||
| ) | ||
|
|
||
| frames = 0 | ||
| pushed_chunks = 0 | ||
| with ExitStack() as stack: | ||
| stack.enter_context(CloudXRLauncher.launch_context(args)) | ||
| data_tracker = TensorPushTracker( | ||
| args.collection_id, "tactile", TACTILE_SAMPLE_SIZE | ||
| ) | ||
| metadata_tracker = TensorPushTracker( | ||
| tracker_metadata_collection_id(args.collection_id), | ||
| TRACKER_METADATA_TENSOR_IDENTIFIER, | ||
| TRACKER_METADATA_CHUNK_SIZE, | ||
| ) | ||
| trackers = [data_tracker, metadata_tracker] | ||
| oxr_session = stack.enter_context( | ||
| OpenXRSession( | ||
| "TactileFeeder", DeviceIOSession.get_required_extensions(trackers) | ||
| ) | ||
| ) | ||
| session = stack.enter_context( | ||
| DeviceIOSession.run(trackers, oxr_session.get_handles()) | ||
| ) | ||
|
|
||
| start = time.monotonic() | ||
| try: | ||
| while args.duration <= 0 or time.monotonic() - start < args.duration: | ||
| remaining = start + frames / args.rate - time.monotonic() | ||
| if remaining > 0: | ||
| time.sleep(remaining) | ||
| session.update() | ||
| t = time.monotonic() - start | ||
| data_tracker.push( | ||
| session, TactileData(mock_forces(args.taxels, t)).to_bytes() | ||
| ) | ||
| for _ in range(args.chunks_per_frame): | ||
| metadata_tracker.push(session, chunks[pushed_chunks % len(chunks)]) | ||
| pushed_chunks += 1 | ||
| frames += 1 | ||
| if frames % max(1, int(args.rate * 5)) == 0: | ||
| logger.info("Published %d frames", frames) | ||
| except KeyboardInterrupt: | ||
|
|
||
| pass | ||
|
|
||
| logger.info("Published %d frames in total", frames) | ||
| return 0 | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| sys.exit(main(sys.argv)) | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.