Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion docs/source/device/tactile/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ Tactile devices
===============

Tactile devices capture contact measurements. For actuator output, see
:doc:`/references/haptic_feedback`.
:doc:`/references/haptic_feedback`. A device can publish through the vendor-neutral
:doc:`/references/tactile_schema`.

.. toctree::
:maxdepth: 1
Expand Down
2 changes: 2 additions & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ Table of Contents
references/requirements
references/build
references/generated_trackers
references/skeleton_binding
references/tactile_schema
references/retargeting/index
references/device_provider_monitoring
references/camera_streaming
Expand Down
7 changes: 5 additions & 2 deletions docs/source/references/generated_trackers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,10 @@ example) declares that description as the tracker's ``metadata`` table:
[[tracker]]
name = "tactile"
table = "TactileData"
metadata = "TactileShape"
metadata = "TactileMetadata"

The metadata table is the ``root_type`` of a schema of its own, named after the table in snake case
(``tactile_shape.fbs``), with no Record wrapper: MCAP records the table itself. Like the data table,
(``tactile_metadata.fbs``), with no Record wrapper: MCAP records the table itself. Like the data table,
it needs a hand-written ``schema/python/*_bindings.h`` before ``get_metadata()`` can return it to
Python. Only ``direction = "pull"`` supports ``metadata``.

Expand Down Expand Up @@ -154,6 +154,9 @@ Generated today
* - ``gamepad``
- ``GamepadTracker``
- Gamepad state pushed by the gamepad plugin (SDL3); only ``python_accessor`` is overridden
* - ``tactile``
- ``TactileTracker``
- Taxel readings with a ``TactileMetadata`` device description; see :doc:`tactile_schema`

Still hand-written
------------------
Expand Down
77 changes: 77 additions & 0 deletions docs/source/references/skeleton_binding.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
.. SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
.. SPDX-License-Identifier: Apache-2.0

Skeleton Binding
================

A device that describes rigid segments — the links a tactile glove's taxels are skinned to,
for example — carries a ``SkeletonBinding`` that names each segment in a skeleton whose pose
some other source provides at run time. The schema is
:code-file:`src/core/schema/fbs/skeleton_binding.fbs`; the registry and the checks below are
:code-file:`src/core/schema/cpp/inc/schema/skeleton_binding.hpp`.

``segment_names[i]`` names segment ``i`` of the device. ``profile`` says which skeleton the names
come from; without it the binding is *generic* and the names are labels only. ``asset`` identifies
an external description, such as a URDF, that a profile names segments in.

Registered profiles
-------------------

.. list-table::
:header-rows: 1

* - ``id``
- ``version``
- Segment names
- Requires
* - ``openxr.hand``
- 1
- ``HandJoint`` names (``PALM`` … ``LITTLE_TIP``), the XR_EXT_hand_tracking joints
- ``side`` LEFT or RIGHT
* - ``openxr.body.bd``
- 1
- ``BodyJoint`` names (``PELVIS`` … ``RIGHT_HAND``), the XR_BD_body_tracking joints
- —
* - ``urdf.link``
- 1
- ``<link name>`` values of the URDF in ``asset``
- ``asset``

A profile's names never change within a version; a change takes a new ``version``. Ids outside
this table are valid and read as generic bindings. Vendor profiles take a reverse-DNS prefix,
such as ``com.example.glove``.

Rules
-----

``validate_skeleton_binding(binding, segment_count, side)`` returns an empty string for a valid
binding, otherwise the first rule it breaks:

#. ``segment_names`` has exactly ``segment_count`` entries, each non-empty and unique.
#. ``asset.digest``, when present, is an RFC 6920 SHA-256 name: ``ni:///sha-256;`` and the
43-character unpadded base64url digest of the asset's bytes.
#. An ``asset`` needs a ``profile``.
#. ``profile.id`` is two or more dot-separated tokens of ``[a-z0-9_]``, each starting with a
letter; ``profile.version`` is at least 1.
#. For a registered profile: ``asset`` is present exactly when the profile requires one, ``side``
is LEFT or RIGHT when the profile requires it, and every name is one of the profile's names.

Checking names against an asset needs its bytes, so it is a separate step in Python:
``check_skeleton_asset(binding, data)`` verifies the digest and, for ``urdf.link``, that every name
is a link of the URDF. ``asset_digest(data)`` computes the digest to store.

.. code-block:: python

from isaaccapture.schema import (
AssetRef, SkeletonBinding, SkeletonProfileRef, asset_digest, check_skeleton_asset,
validate_skeleton_binding,
)

urdf = open("glove.urdf", "rb").read()
binding = SkeletonBinding(
["base_link", "index_tip"],
profile=SkeletonProfileRef("urdf.link", 1),
asset=AssetRef(asset_digest(urdf), "package://glove/glove.urdf", "application/xml"),
)
assert validate_skeleton_binding(binding, segment_count=2) == ""
assert check_skeleton_asset(binding, urdf) == ""
70 changes: 70 additions & 0 deletions docs/source/references/tactile_schema.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
.. SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
.. SPDX-License-Identifier: Apache-2.0

Tactile Schema
==============

A tactile device publishes through the generated ``TactileTracker``: a ``TactileMetadata`` once per
session as the tracker's :ref:`metadata <tracker-metadata>`, and a ``TactileData`` every frame. The
schemas are :code-file:`src/core/schema/fbs/tactile_metadata.fbs` and
:code-file:`src/core/schema/fbs/tactile.fbs`; the checks below are
:code-file:`src/core/schema/cpp/inc/schema/tactile_validation.hpp`.

Device description
------------------

``TactileMetadata`` holds:

- ``device`` — vendor, model, serial number, firmware, ``side`` (a ``BodySide``), nominal rate and
calibration id.
- ``shape`` — the rigid ``segments`` of the device, each with its bind pose, and the ``taxels``, each
with its pose and up to four skinning weights over the segments. Both are in the device frame at
the reference configuration. ``taxel_areas`` optionally gives each taxel's area in m².
- ``binding`` — a :doc:`skeleton_binding` naming each segment in the skeleton that poses it. Without
one the segments are not posed by anything and the taxels stay at their bind poses.
- ``channels`` — which ``TactileData`` vectors the device fills.
- ``extensions`` — vendor data, each named by a registry id such as ``com.example.glove``.

A taxel's frame has +Z along the outward surface normal and +X along the shear x axis. Its position
at run time is

.. code-block:: text

p = sum_k weights[k] * T_current[segments[k]] * inverse(T_bind[segments[k]]) * p_bind

where ``T_current`` is the segment's pose from the bound skeleton, and ``p`` comes out in the frame
that skeleton is posed in. The taxel's axes blend the same way and are renormalized.

Per-frame data
--------------

Entry *i* of each ``TactileData`` vector is taxel *i* of the shape. Forces are what the contacted
surface exerts on the taxel, in newtons:

- ``normal_force`` — along the taxel normal; positive presses into the surface.
- ``shear_force`` — along the taxel frame's +X and +Y.
- ``raw`` — ``channels.raw_values_per_taxel`` uncalibrated readings per taxel.
- ``taxel_state`` — ``VALID``, ``SATURATED``, ``INVALID`` or ``UNAVAILABLE``; when absent, every
taxel is ``VALID``.
- ``sequence`` — the device's frame counter, when it has one.

Rules
-----

``validate_tactile_metadata(metadata)`` returns an empty string for a valid description, otherwise
the first rule it breaks. The Python ``TactileMetadata`` constructor raises ``ValueError`` with the
same message.

#. ``shape`` has at least one segment and one taxel.
#. Every bind pose and taxel pose is finite, with an orientation of norm 1 within 1e-3.
#. Each taxel's weights are finite and nonnegative and sum to 1 within 1e-5. A slot with a nonzero
weight names an existing segment that no other slot of the taxel names; a zero-weight slot
holds segment 0.
#. ``taxel_areas`` is empty or holds one finite, positive value per taxel.
#. ``channels`` is present and declares normal force, shear force or raw readings.
#. ``binding``, when present, passes ``validate_skeleton_binding`` for the shape's segments and
``device.side``.
#. Extension ids are registry ids and unique.

``validate_tactile_data(data, metadata)`` checks that a frame fills exactly the declared vectors,
with one entry per taxel (``raw_values_per_taxel`` per taxel for ``raw``).
1 change: 1 addition & 0 deletions examples/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ if(BUILD_EXAMPLES)
add_subdirectory(deviceio_live_view)
add_subdirectory(soma_body_publisher)
add_subdirectory(soma_hand_publisher)
add_subdirectory(tactile)
add_subdirectory(haptic_feedback)
add_subdirectory(sharpa_avatar)
add_subdirectory(vehicle_teleop)
Expand Down
7 changes: 7 additions & 0 deletions examples/tactile/CMakeLists.txt
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)
30 changes: 30 additions & 0 deletions examples/tactile/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<!--
SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
SPDX-License-Identifier: Apache-2.0
-->

# Tactile feeder and printer

1. In the Python environment that has Isaac Capture installed, install the example from the
repository root:

```bash
uv pip install -e ./examples/tactile
```

2. Start the printer. It starts the CloudXR runtime if none is running:

```bash
python -m isaaccapture_examples.tactile.printer --accept-eula
```

3. In a second terminal with the same environment, start the feeder:

```bash
python -m isaaccapture_examples.tactile.feeder
```

4. In the printer terminal, check that a `TactileMetadata` block is printed once, followed by a
`TactileData` block with the values of taxel 0 and taxel 15 every second.

5. Press `Ctrl+C` in the feeder terminal, then in the printer terminal.
23 changes: 23 additions & 0 deletions examples/tactile/pyproject.toml
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 Capture 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 }
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# 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"

# TactileTracker's default max_flatbuffer_size (trackers.toml); a larger collection is rejected.
TACTILE_MAX_PAYLOAD_SIZE = 32768
Loading
Loading