Skip to content
Merged
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
46 changes: 46 additions & 0 deletions cmake/GenerateTrackers.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

function(isaac_teleop_generate_trackers)
set(_TRACKER_MANIFEST "${CMAKE_SOURCE_DIR}/src/core/deviceio_trackers/trackers.toml")
set(_TRACKER_DEFAULTS "${CMAKE_SOURCE_DIR}/src/core/deviceio_trackers/defaults.toml")
set(_GENERATOR "${CMAKE_SOURCE_DIR}/src/core/codegen/generate_trackers.py")
set(_CODEGEN_TEMPLATES_DIR "${CMAKE_SOURCE_DIR}/src/core/codegen/templates")
file(GLOB_RECURSE _CODEGEN_IN_TEMPLATES CONFIGURE_DEPENDS
"${_CODEGEN_TEMPLATES_DIR}/*.template")
set(_OUT_DIR "${CMAKE_BINARY_DIR}/generated/trackers")
set(_CMAKE_OUT "${_OUT_DIR}/generated_sources.cmake")

# Pass --prune-stale so renamed/removed trackers cannot leave orphan headers that
# still satisfy stale #includes on the generated include path.
execute_process(
COMMAND "${Python3_EXECUTABLE}" "${_GENERATOR}"
--manifest "${_TRACKER_MANIFEST}"
--defaults "${_TRACKER_DEFAULTS}"
--out-dir "${_OUT_DIR}"
--emit-cmake "${_CMAKE_OUT}"
--prune-stale
RESULT_VARIABLE _tracker_gen_rc
OUTPUT_VARIABLE _tracker_gen_out
ERROR_VARIABLE _tracker_gen_err)
if(NOT _tracker_gen_rc EQUAL 0)
message(FATAL_ERROR "tracker codegen failed:\n${_tracker_gen_out}\n${_tracker_gen_err}")
endif()

include("${_CMAKE_OUT}")
set(GENERATED_TRACKER_DIR "${GENERATED_TRACKER_DIR}" PARENT_SCOPE)
set(GENERATED_TRACKER_FACADE_SOURCES "${GENERATED_TRACKER_FACADE_SOURCES}" PARENT_SCOPE)
set(GENERATED_TRACKER_LIVE_SOURCES "${GENERATED_TRACKER_LIVE_SOURCES}" PARENT_SCOPE)
set(GENERATED_TRACKER_REPLAY_SOURCES "${GENERATED_TRACKER_REPLAY_SOURCES}" PARENT_SCOPE)
set(GENERATED_TRACKER_DEVICEIO_BASE_INC_DIR "${GENERATED_TRACKER_DEVICEIO_BASE_INC_DIR}" PARENT_SCOPE)
set(GENERATED_TRACKER_DEVICEIO_TRACKERS_INC_DIR "${GENERATED_TRACKER_DEVICEIO_TRACKERS_INC_DIR}" PARENT_SCOPE)
set(GENERATED_TRACKER_LIVE_IMPL_INC_DIR "${GENERATED_TRACKER_LIVE_IMPL_INC_DIR}" PARENT_SCOPE)
set(GENERATED_TRACKER_REPLAY_IMPL_INC_DIR "${GENERATED_TRACKER_REPLAY_IMPL_INC_DIR}" PARENT_SCOPE)
set(GENERATED_TRACKER_INC_DIR "${GENERATED_TRACKER_INC_DIR}" PARENT_SCOPE)
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS
"${_TRACKER_MANIFEST}" "${_TRACKER_DEFAULTS}" "${_GENERATOR}"
"${CMAKE_SOURCE_DIR}/src/core/codegen/manifest.py"
"${CMAKE_SOURCE_DIR}/src/core/codegen/templates.py"
"${CMAKE_SOURCE_DIR}/src/core/codegen/template_renderer.py"
${_CODEGEN_IN_TEMPLATES})
endfunction()
85 changes: 67 additions & 18 deletions docs/source/device/add_device.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,10 @@ The reference implementation is the **generic 3-axis foot pedal**:
* - Device Plugin
- :code-dir:`src/plugins/generic_3axis_pedal`
* - Tracker facade (``Generic3AxisPedalTracker``)
- :code-file:`src/core/deviceio_trackers/cpp/generic_3axis_pedal_tracker.cpp` and
:code-file:`src/core/deviceio_trackers/cpp/inc/deviceio_trackers/generic_3axis_pedal_tracker.hpp`
- Generated at configure time from :code-file:`src/core/deviceio_trackers/trackers.toml`
(see :ref:`Schema-based tracker manifest <schema-based-tracker-manifest>`)
* - Live backend (``LiveGeneric3AxisPedalTrackerImpl``)
- :code-file:`src/core/live_trackers/cpp/live_generic_3axis_pedal_tracker_impl.cpp` and
:code-file:`src/core/live_trackers/cpp/live_generic_3axis_pedal_tracker_impl.hpp`
- Generated alongside the facade under ``${CMAKE_BINARY_DIR}/generated/trackers/``
* - ``SchemaTracker`` / ``SampleResult``
- :code-file:`src/core/live_trackers/cpp/inc/live_trackers/schema_tracker.hpp`
* - Debug printer
Expand Down Expand Up @@ -107,11 +106,57 @@ Reference implementation: :code-dir:`src/plugins/generic_3axis_pedal`. The plugi
Step 3: Implement a tracker
----------------------------

.. _schema-based-tracker-manifest:

Schema-based trackers (manifest)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

If the device is a **schema-based FlatBuffer tracker** over OpenXR tensor collections (plugin
serializes a table; the host reads or writes it via ``SchemaTracker`` / ``SchemaPusher`` with no
``xrLocate*`` calls and no custom connection state), you usually **do not** hand-write the
seven-layer tracker stack. Instead:

1. Add or extend the FlatBuffer schema (Step 1).
2. Add a ``[[tracker]]`` entry to :code-file:`src/core/deviceio_trackers/trackers.toml`. Defaults
in :code-file:`src/core/deviceio_trackers/defaults.toml` expand ``%name%``, ``%name_CamelCase%``,
MCAP channel names, and class names; override only genuine exceptions (``se3_tracker`` uses
``class = "Se3Tracker"``; pedals use ``schema = "pedals"`` and ``channel = "pedals"``).
3. Reconfigure/rebuild. ``cmake/GenerateTrackers.cmake`` runs
:code-file:`src/core/codegen/generate_trackers.py` and wires generated sources into
``deviceio_trackers``, ``live_trackers``, and ``replay_trackers``.

Use ``direction = "push"`` when Teleop **pushes** a typed table to a plugin (e.g.
``HapticCommandPushTracker``). Cross-process **consumers** that bucket multiple
``HapticCommand.endpoint`` samples on one collection (e.g. ``HapticCommandReaderTracker`` for Manus
haptics) stay hand-written — see :doc:`../references/generated_trackers`.

For what the generator emits, which trackers it covers today, and which shapes are not supported
yet, see :doc:`../references/generated_trackers`.

Diagnose surprising defaults with::

python src/core/codegen/generate_trackers.py \\
--manifest src/core/deviceio_trackers/trackers.toml \\
--defaults src/core/deviceio_trackers/defaults.toml \\
--out-dir /tmp/isaac-teleop-trackers \\
--emit-cmake /tmp/isaac-teleop-trackers.cmake \\
--print-resolved
Comment thread
nv-jakob marked this conversation as resolved.

(``--out-dir`` and ``--emit-cmake`` are required by the CLI even when ``--print-resolved``
skips writing generated sources; temporary paths are fine.)

Hand-written trackers
~~~~~~~~~~~~~~~~~~~~~

Trackers that are not schema-based FlatBuffer readers/writers (OpenXR ``xrLocate*``, opaque message
channels, multi-endpoint haptic readers, …) still use the manual facade + live/replay impl pattern
below.

Comment thread
nv-jakob marked this conversation as resolved.
The tracker runs inside a consumer process (e.g. Teleop pipeline or a small reader app). It
implements the **ITracker** interface (tracker **facade** in ``deviceio_trackers``); the
**live backend** in ``live_trackers`` composes **SchemaTracker** to read raw
tensor samples from OpenXR. Implement a concrete tracker class (e.g.
``Generic3AxisPedalTracker``) that:
``HandTracker``) that:

- **Extends ITracker** — Override ``get_name()``,
``get_schema_name()``, ``get_schema_text()``, and ``get_record_channels()``.
Expand Down Expand Up @@ -141,19 +186,23 @@ In the **Impl**:
device disappeared and there are no samples, you may emit one record with null data and
the update-tick timestamp so the MCAP stream marks absence.

Reference implementation — split across facade and live backend:

- **Tracker facade** — :code-file:`src/core/deviceio_trackers/cpp/generic_3axis_pedal_tracker.cpp`
(class ``Generic3AxisPedalTracker``): holds collection configuration, implements ``ITracker``, and
exposes ``get_data(session)`` returning
``Generic3AxisPedalOutputTrackedT`` by dispatching to the session’s
``IGeneric3AxisPedalTrackerImpl`` (see :code-file:`src/core/deviceio_base/cpp/inc/deviceio_base/generic_3axis_pedal_tracker_base.hpp`).
- **Live backend** — :code-file:`src/core/live_trackers/cpp/live_generic_3axis_pedal_tracker_impl.cpp`
(``LiveGeneric3AxisPedalTrackerImpl``): composes ``SchemaTracker``, implements ``update()`` and
``serialize_all()``, and uses ``SchemaTracker::read_all_samples()`` with
``std::vector<SchemaTracker::SampleResult>`` for the pending batch. See
:code-file:`src/core/live_trackers/cpp/inc/live_trackers/schema_tracker.hpp`
for ``SchemaTracker`` and ``SampleResult`` (buffer + timestamp metadata).
Reference implementation — generated ``SchemaTracker`` reader and a hand-written OpenXR
locate tracker:

- **Generated schema-based reader** — after configure, under
``${CMAKE_BINARY_DIR}/generated/trackers/``: facade
``deviceio_trackers/generic_3axis_pedal_tracker.cpp`` (``Generic3AxisPedalTracker``), base
``deviceio_base/generic_3axis_pedal_tracker_base.hpp``, and live backend
``live_trackers/live_generic_3axis_pedal_tracker_impl.cpp``
(``LiveGeneric3AxisPedalTrackerImpl``). The live impl composes ``SchemaTracker`` and uses
``read_all_samples()`` with ``std::vector<SchemaTrackerBase::SampleResult>``. See
:code-file:`src/core/live_trackers/cpp/inc/live_trackers/schema_tracker.hpp` for
``SchemaTracker`` / ``SampleResult``.
- **Hand-written OpenXR locate tracker** — facade
:code-file:`src/core/deviceio_trackers/cpp/hand_tracker.cpp` (``HandTracker``) and live
backend :code-file:`src/core/live_trackers/cpp/live_hand_tracker_impl.cpp`
(``LiveHandTrackerImpl``): same facade / ``ITrackerImpl`` split, but ``update()`` drives
``xrLocate*`` rather than ``SchemaTracker``.

Step 4: Implement a simple C++ printer (optional)
-------------------------------------------------
Expand Down
5 changes: 3 additions & 2 deletions docs/source/device/trackers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ APIs (``xrLocateSpace``, ``xrSyncActions``, etc.):
reading it from OpenXR tensor collections via the
:code-file:`SchemaTracker <src/core/live_trackers/cpp/inc/live_trackers/schema_tracker.hpp>` utility.

- :code-file:`FrameMetadataTrackerOak <src/core/deviceio_trackers/cpp/inc/deviceio_trackers/frame_metadata_tracker_oak.hpp>` -- frame metadata for one OAK camera stream
- :code-file:`FrameMetadataTrackerOak <src/core/deviceio_trackers/trackers.toml>` -- frame metadata for one OAK camera stream (generated)
- :code-file:`Generic3AxisPedalTracker <src/core/deviceio_trackers/cpp/inc/deviceio_trackers/generic_3axis_pedal_tracker.hpp>` -- foot pedal axis values
- :code-file:`JointStateTracker <src/core/deviceio_trackers/cpp/inc/deviceio_trackers/joint_state_tracker.hpp>` -- named joint-space device state (leader arms, exoskeletons, gloves, ...)
- :code-file:`Se3Tracker <src/core/deviceio_trackers/cpp/inc/deviceio_trackers/se3_tracker.hpp>` -- generic SE3 (6-DoF) pose sources (tracker pucks, mocap rigid bodies, logical trackers)
Expand Down Expand Up @@ -227,7 +227,8 @@ stream, passing the tensor collection the plugin publishes that stream under --
utility internally.

- Schema: :code-file:`src/core/schema/fbs/oak.fbs`
- C++ header: ``#include <deviceio/frame_metadata_tracker_oak.hpp>``
- Manifest: :code-file:`src/core/deviceio_trackers/trackers.toml` (``frame_metadata_oak``)
- C++ header: ``#include <deviceio_trackers/frame_metadata_tracker_oak.hpp>``
- Python import: ``from isaacteleop.deviceio import FrameMetadataTrackerOak``
- Record channels: ``oak``, ``oak_tracked`` | MCAP schema: ``core.FrameMetadataOakRecord``
- Tests:
Expand Down
1 change: 1 addition & 0 deletions docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ Table of Contents

references/requirements
references/build
references/generated_trackers
references/retargeting/index
references/camera_streaming
references/mcap_record_replay
Expand Down
163 changes: 163 additions & 0 deletions docs/source/references/generated_trackers.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
.. SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
.. SPDX-License-Identifier: Apache-2.0

Generated Tracker Code
======================

Trackers whose live impl only moves a FlatBuffer between a plugin and the host via
``SchemaTracker`` or ``SchemaPusher`` — *schema-based* trackers — are not hand-written. They are
declared in a TOML manifest and their C++ and Python glue is generated when you configure the build.
Adding one is a ``.fbs`` schema plus roughly ten lines of TOML; see
:ref:`Schema-based trackers (manifest) <schema-based-tracker-manifest>` for the authoring workflow.

This page records what is generated today, what is deliberately still hand-written, and what
would have to change to generate the rest.

How it works
------------

Three inputs drive the generator:

- :code-file:`src/core/deviceio_trackers/trackers.toml` — one ``[[tracker]]`` entry per tracker,
carrying only the values that differ from the defaults.
- :code-file:`src/core/deviceio_trackers/defaults.toml` — the defaults, expressed with
``%placeholder%`` substitutions (``%name%``, ``%name_CamelCase%``, and so on).
- :code-dir:`src/core/codegen` — ``manifest.py`` resolves the placeholders, ``templates.py``
derives type and file names, ``templates/`` holds the C++ bodies as ``*.hpp.template`` /
``*.cpp.template`` under ``pull/`` and ``push/`` (``@KEY@`` substitution via ``template_renderer.py``),
and ``generate_trackers.py`` renders the output.

:code-file:`cmake/GenerateTrackers.cmake` runs the generator through ``execute_process`` at
**configure** time, not build time, because CMake needs the resulting source list before it can
define targets. Output lands in ``${CMAKE_BINARY_DIR}/generated/trackers/`` and is **not** checked
into git, exactly like the ``flatc`` output. Sources are wired through an explicit
``generated_sources.cmake`` list (not by globbing that directory). Two consequences worth knowing:

- Grepping ``src/`` for a generated class such as ``Se3Tracker`` finds the manifest entry and the
generator, not a ``.cpp``. Searching, debugging, and clangd all need a configured build tree.
- The manifests, every ``.py`` under ``src/core/codegen``, and every template under
``templates/`` are registered as ``CMAKE_CONFIGURE_DEPENDS``, so editing any of them re-runs
configure on the next build.

The generator rewrites a file only when its content changed. Configure also passes
``--prune-stale`` so that after renaming or removing a tracker, orphan headers cannot remain on
the generated include path and keep satisfying a stale ``#include``.

What each entry produces
------------------------

Per manifest entry, in ``${CMAKE_BINARY_DIR}/generated/trackers/``:

.. code-block:: text
:class: code-100col

deviceio_base/<header>_base.hpp # I<Class>Impl interface
deviceio_trackers/inc/deviceio_trackers/<header>.hpp
deviceio_trackers/<header>.cpp # the ITracker facade
live_trackers/live_<header>_impl.{hpp,cpp} # wraps SchemaTracker / SchemaPusher
replay_trackers/replay_<header>_impl.{hpp,cpp}

``header`` defaults from ``name`` (``<name>_tracker``, or ``name`` when it already ends in
``_tracker``). Override it when the public ``#include`` stem must keep a historical path.

The shared registration points stay hand-written and ``#include`` a generated ``.inc`` fragment,
so hand-written trackers keep their own rows. The fragments under ``generated/trackers/inc/`` cover
the live and replay factories (includes, forward declarations, try-create thunks, dispatch rows,
factory declarations and definitions), the MCAP recording traits in
:code-file:`src/core/mcap/cpp/inc/mcap/recording_traits.hpp`, the pybind blocks in
:code-file:`src/core/deviceio_trackers/python/tracker_bindings.cpp`, and a
``_generated_tracker_exports.py`` that :code-file:`src/python/isaacteleop/deviceio_trackers/__init__.py`
star-imports. Because that last one is spliced into ``__all__``, a new manifest entry needs no
Python edit at all.

``.pyi`` stubs need no work either: :code-file:`src/core/python/generate_stubs.py` derives them from
the built module.

Generated today
---------------

.. list-table::
:header-rows: 1
:widths: 26 26 48

* - Manifest entry
- Class
- Notes
* - ``joint_state``
- ``JointStateTracker``
- One generic joint-space device (leader arm, exoskeleton, ...)
* - ``se3_tracker``
- ``Se3Tracker``
- Overrides ``class``; the default would give ``Se3TrackerTracker``
* - ``oglo_tactile``
- ``OgloTactileTracker``
- MCAP channels are ``oglo``/``oglo_tracked``, so ``channel`` is overridden
* - ``generic_3axis_pedal``
- ``Generic3AxisPedalTracker``
- Schema lives in ``pedals.fbs``, so ``schema`` and ``channel`` are overridden
* - ``haptic_command``
- ``HapticCommandPushTracker``
- ``direction = "push"``: a typed producer wrapping ``SchemaPusher``
* - ``frame_metadata_oak``
- ``FrameMetadataTrackerOak``
- Overrides ``class`` and ``header`` (file stem ``frame_metadata_tracker_oak``)

Still hand-written
------------------

.. list-table::
:header-rows: 1
:widths: 34 66

* - Tracker
- Why it is not generated
* - ``HeadTracker``, ``HandTracker``, ``ControllerTracker``
- Real ``xrLocate*`` / hand-tracking calls, not schema-based FlatBuffer readers
* - ``FullBodyTracker`` (PICO vendor)
- Native ``XR_BD_body_tracking``
* - ``FullBodyTracker`` (Noitom vendor)
- Genuinely schema-based by mechanism, but a **vendor** of an existing facade — see below
* - ``MessageChannelTracker``
- ``XR_NV_opaque_data_channel`` with its own connection state machine
* - ``HapticCommandReaderTracker``
- Cross-process consumer: ``read_all_samples`` on one collection, buckets by
``HapticCommand.endpoint`` (left/right). Paired with generated ``HapticCommandPushTracker``.
* - ``TensorPushTracker``
- Deliberately kept as the untyped ``bytes`` escape hatch

A quick way to tell the two groups apart: only schema-based impls mention ``SchemaTracker`` or
``SchemaPusher``. Of the hand-written live impls that use those helpers, each is listed above.

Future work
-----------

**Multi-endpoint reader (rejected generated shape).** We briefly generated
``HapticCommandReaderTracker`` with a ``multi_endpoint`` template (bucket samples by
``HapticCommand.endpoint`` on one push-tensor collection). That shape was removed: it made the
codegen tree branchy for a single device, and a generic multi-endpoint reader does not fit
``pull/`` or ``push``. The hand-written reader remains the supported approach; a future redesign
(split tensors/collections per endpoint, or a dedicated generator) is optional follow-up.

**Vendored shape (Noitom).** ``LiveFullBodyTrackerNoitomImpl`` reads
``SchemaTracker<FullBodyPoseRecord, FullBodyPose>``, so it is schema-based by mechanism, but it is a
second *vendor* of the hand-written ``FullBodyTracker`` rather than its own tracker type. Generating
it would need a shape that emits **only** a live impl and a vendor-keyed dispatch row — no facade,
base interface, recording traits, replay half, or pybind block, since it shares all of those with the
PICO vendor. Its ``collection_id`` and ``max_flatbuffer_size`` also arrive through
``TrackerVendor::params`` at runtime instead of being fixed at generation time.

**Schema pybind bindings.** The field-by-field ``src/core/schema/python/*_bindings.h`` files are
Comment thread
nv-jakob marked this conversation as resolved.
still hand-written. They are derivable from the ``.bfbs`` reflection data ``flatc`` already emits
(``--bfbs-gen-embed --reflect-names --reflect-types``), so this is a viable follow-up, but it is a
separate generator with its own risks.

**Manifest ``python_accessor`` (pybind method name).** Generated ``pull`` trackers use
``fragments/pybind_pull.template``, which binds whatever string ``python_accessor`` holds (default
``get_data``; ``oglo_tactile`` and ``generic_3axis_pedal`` override with device-specific names).
``direction = "push"`` uses ``push`` via defaults. Future work: remove the manifest key and
hard-code ``get_data`` on readers (or maybe ``pull``, paired with ``push`` on producers).
Maybe rename the C++ facade to match if we pick ``pull``, so Python and C++ stay aligned without
per-tracker overrides.

If a prospective shape makes the templates hard to follow, leaving that tracker hand-written is the
correct outcome — the generator exists to remove mechanical duplication, not to model every device.
Loading
Loading