diff --git a/cmake/ClangFormat.cmake b/cmake/ClangFormat.cmake index b0500093e..fe210c3d6 100644 --- a/cmake/ClangFormat.cmake +++ b/cmake/ClangFormat.cmake @@ -36,6 +36,7 @@ set(_cf_patterns ) set(_cf_exclude_patterns + ".*\\.template$" ".*/build/.*" ".*/deps/.*" ".*/manus/ManusSDK/.*" diff --git a/cmake/GenerateTrackers.cmake b/cmake/GenerateTrackers.cmake new file mode 100644 index 000000000..242c5b3b7 --- /dev/null +++ b/cmake/GenerateTrackers.cmake @@ -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/in_renderer.py" + ${_CODEGEN_IN_TEMPLATES}) +endfunction() diff --git a/docs/source/device/add_device.rst b/docs/source/device/add_device.rst index 5ad1dd792..d7bbf1fab 100644 --- a/docs/source/device/add_device.rst +++ b/docs/source/device/add_device.rst @@ -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:`Passthrough 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 @@ -107,11 +106,55 @@ Reference implementation: :code-dir:`src/plugins/generic_3axis_pedal`. The plugi Step 3: Implement a tracker ---------------------------- +.. _passthrough-tracker-manifest: + +Passthrough trackers (manifest) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If the device is a **pure FlatBuffer passthrough** over OpenXR tensor collections (plugin serializes +a table; the host reads it unchanged), 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 = "out"`` 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 + +(``--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 +~~~~~~~~~~~~~~~~~~~~~ + +Non-passthrough trackers (OpenXR ``xrLocate*``, multi-stream camera metadata, opaque message +channels, …) still use the manual facade + live/replay impl pattern below. + 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()``. @@ -141,19 +184,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`` 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 — SchemaTracker passthrough (generated) and a hand-written OpenXR +locate tracker: + +- **Generated SchemaTracker passthrough** — 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``. 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) ------------------------------------------------- diff --git a/docs/source/index.rst b/docs/source/index.rst index 9415d44ee..a16af2a93 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -77,6 +77,7 @@ Table of Contents references/requirements references/build + references/generated_trackers references/retargeting/index references/camera_streaming references/mcap_record_replay diff --git a/docs/source/references/generated_trackers.rst b/docs/source/references/generated_trackers.rst new file mode 100644 index 000000000..328155c41 --- /dev/null +++ b/docs/source/references/generated_trackers.rst @@ -0,0 +1,165 @@ +.. SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +.. SPDX-License-Identifier: Apache-2.0 + +Generated Tracker Code +====================== + +Trackers that only move a FlatBuffer between a plugin and the host — *passthrough* 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:`Passthrough trackers (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`` (``@KEY@`` substitution via ``in_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/_tracker_base.hpp # ITrackerImpl interface + deviceio_trackers/inc/deviceio_trackers/_tracker.hpp + deviceio_trackers/_tracker.cpp # the ITracker facade + live_trackers/live__tracker_impl.{hpp,cpp} # wraps SchemaTracker / SchemaPusher + replay_trackers/replay__tracker_impl.{hpp,cpp} + +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 = "out"``: a typed producer wrapping ``SchemaPusher`` + +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 FlatBuffer passthrough + * - ``FullBodyTracker`` (PICO vendor) + - Native ``XR_BD_body_tracking`` + * - ``FullBodyTracker`` (Noitom vendor) + - Genuinely a passthrough, 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``. + * - ``FrameMetadataTrackerOak`` + - Holds N ``SchemaTracker`` s, one collection per stream — see below + * - ``TensorPushTracker`` + - Deliberately kept as the untyped ``bytes`` escape hatch + +A quick way to tell the two groups apart: only passthrough 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 +``in/`` or ``out``. The hand-written reader remains the supported approach; a future redesign +(split tensors/collections per endpoint, or a dedicated generator) is optional follow-up. + +**Multi-stream shape (OAK).** ``FrameMetadataTrackerOak`` owns one ``SchemaTracker`` per camera +stream, with collection ids derived as ``{prefix}/{StreamName}``. It needs a template variant that +emits an N-collection reader plus per-stream accessors. This was sequenced last during the initial +migration precisely so it could not distort the common template, and it remains the closest +candidate. + +**Vendored shape (Noitom).** ``LiveFullBodyTrackerNoitomImpl`` reads +``SchemaTracker``, so it is passthrough 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 +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 ``in`` trackers use +``fragments/pybind_in.template``, which binds whatever string ``python_accessor`` holds (default +``get_data``; ``oglo_tactile`` and ``generic_3axis_pedal`` override with device-specific names). +``direction = "out"`` 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. diff --git a/src/core/AGENTS.md b/src/core/AGENTS.md index 1b5868aa0..4e5bfd5e8 100644 --- a/src/core/AGENTS.md +++ b/src/core/AGENTS.md @@ -20,5 +20,6 @@ If work under **`src/core/`** went wrong—**user** correction, **pre-commit/CI* - After Python test or session-manager edits, let `ruff format`/pre-commit own wrapping and rerun the hook when it modifies files. - **`IDeviceIOSource` leaves are only discovered when reachable from a declared `OutputCombiner` output.** `TeleopSession._discover_sources` calls `pipeline.get_leaf_nodes()`, which walks back from the combiner's outputs. An input *source* whose only purpose is a side effect (e.g. message-channel send) must therefore expose at least one output (a heartbeat boolean is the established pattern) **and** the user's combiner must include it; silent no-discovery is the recurring footgun. **Output `IDeviceIOSink` nodes are different:** they are registered via `TeleopSessionConfig(sinks=[...])` and the session runs and flushes them explicitly each frame, so a sink needs no heartbeat output and no `OutputCombiner` reachability. - **Run `clang-format` on touched C++ before pushing** — CI rejects unformatted C++ and pre-commit does not catch it. See the formatting instructions in the repo root [`AGENTS.md`](../../AGENTS.md) ("Pre-commit — match CI before you stop") for the exact commands; the source of truth lives there, not here. -- **Authored `.py` under `src/python/isaacteleop/` carries no build-system registration.** The `CONFIGURE_DEPENDS` glob in ``src/python/CMakeLists.txt`` stages every `.py` there and `[tool.setuptools.packages.find]` discovers the packages, so a subpackage is a directory with an `__init__.py` and nothing else — no packages list, no `DEPENDS` entry. Targets that *build* a file into the staging tree (an extension `.so`, a generated stub, a vendored runtime) are the exception: each needs an explicit `DEPENDS` on ``src/core/python/CMakeLists.txt::python_package``, or Windows ninja races the wheel build against it, producing ``error: package directory 'isaacteleop\\' does not exist`` from setuptools. +- **Authored `.py` under `src/python/isaacteleop/` carries no build-system registration.** The `CONFIGURE_DEPENDS` glob in ``src/python/CMakeLists.txt`` stages every `.py` there and `[tool.setuptools.packages.find]` discovers the packages, so a subpackage is a directory with an `__init__.py` and nothing else — no packages list, no `DEPENDS` entry. Targets that *produce* files in the staging tree (pybind ``.so``/``.pyd``, ``isaacteleop_python`` copies, ``stage_generated_tracker_exports``, ``viz_py``, vendored runtimes, …) must be **dependencies of** ``python_package`` (``DEPENDS`` / ``add_dependencies(python_package …)`` in ``src/core/python/CMakeLists.txt``), not the other way around — otherwise Windows ninja races the wheel build and setuptools fails with ``error: package directory 'isaacteleop\\' does not exist``. **``python_package`` waits for ``viz_py`` when ``BUILD_VIZ=ON``** (``.pyd`` link race). **Central stubgen** in ``src/core/python/CMakeLists.txt`` (not a ``viz_py`` POST_BUILD) DEPENDS on ``python_package``, so ``stage_generated_tracker_exports`` must stay on ``python_package``'s DEPENDS — otherwise stubbing ``isaacteleop.viz._viz`` (or any module that loads ``isaacteleop/__init__.py``) fails on the missing export. **For ``pip install -e .`` (scikit-build-core), ``_generated_tracker_exports.py`` must also be ``install(FILES ...)`` under ``isaacteleop_wheel``** — the directory install excludes ``*.py`` so authored sources stay redirect-resolved, but that module is not under ``src/python/``. +- **Passthrough tracker sources exist only in a configured build tree.** They are generated at CMake *configure* time from `deviceio_trackers/trackers.toml` into `${CMAKE_BINARY_DIR}/generated/trackers/`, so grepping `src/` for a tracker such as `Se3Tracker` finds the manifest entry and the generator, not a `.cpp`. Searching, debugging, and clangd all need `cmake -B build` to have run first (already true for the `flatc` output). See [`deviceio_trackers/AGENTS.md`](deviceio_trackers/AGENTS.md) and [`codegen/AGENTS.md`](codegen/AGENTS.md) before adding or editing a tracker. - **CloudXR join-main (Orin):** after the native service is up, do not start additional Python threads (including a ``sigwait`` helper). That re-triggers ``_PyGILState_NoteThreadState: Couldn't create autoTSSkey mapping``. Keep ``nv_cxr_service_join`` on the main thread only; rely on the launcher process teardown for shutdown while join blocks. diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index b208dcb95..da30cc497 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -21,11 +21,10 @@ add_subdirectory(oxr) # Build PusherIO library (depends on oxr for session management) add_subdirectory(pusherio) -# DeviceIO: 4-module design -# 1. deviceio_base — abstract interfaces (ITracker, typed impl bases) -# 2. deviceio_trackers — public tracker classes (HeadTracker, HandTracker, etc.) -# 3. live_trackers — live OpenXR tracker impls (depends on trackers) # 4. deviceio_session — DeviceIOSession (wires trackers to live factory) +include(${CMAKE_SOURCE_DIR}/cmake/GenerateTrackers.cmake) +isaac_teleop_generate_trackers() + add_subdirectory(deviceio_base) add_subdirectory(deviceio_trackers) add_subdirectory(live_trackers) diff --git a/src/core/codegen/AGENTS.md b/src/core/codegen/AGENTS.md new file mode 100644 index 000000000..1f295b769 --- /dev/null +++ b/src/core/codegen/AGENTS.md @@ -0,0 +1,80 @@ + + +# Agent notes — `codegen` + +**CRITICAL (non-optional):** Before editing this package, complete the mandatory **`AGENTS.md` preflight** in [`../../../AGENTS.md`](../../../AGENTS.md) (read every applicable `AGENTS.md` on your paths, not just this file). + +This package generates the passthrough tracker stack from +[`../deviceio_trackers/trackers.toml`](../deviceio_trackers/trackers.toml) + +[`../deviceio_trackers/defaults.toml`](../deviceio_trackers/defaults.toml). `manifest.py` resolves +`%placeholder%` values, `templates.py` derives per-entry type/file names, `in_renderer.py` loads +`templates/` bodies with `@KEY@` substitution, and `generate_trackers.py` renders the sources +and the `.inc` fragments. + +## Boundaries + +- **Stdlib only, and this one is load-bearing.** The generator runs at CMake configure time from + whatever interpreter `Python3_EXECUTABLE` points at — a bare uv-managed CPython with no project + venv and no site-packages. A third-party import here is not a dependency, it is a configure-time + crash. That is why the manifests are TOML (`tomllib` is stdlib on 3.11+, the supported floor) and + why there is no Jinja2. +- **Generation happens at configure time, not build time**, because CMake needs the source list up + front. Anything that changes the output (both TOMLs, every `.py` here, and every template under + `templates/`) must stay listed in + `CMAKE_CONFIGURE_DEPENDS` in [`../../../cmake/GenerateTrackers.cmake`](../../../cmake/GenerateTrackers.cmake), + or incremental builds will silently use stale output. +- **Emitted files use compare-before-write.** Content is rewritten only when it differs from the + on-disk copy, so no-op configure runs keep mtimes stable and avoid needless rebuilds. Output + lands under `${CMAKE_BINARY_DIR}/generated/trackers/` (explicit cmake source lists — not a + filesystem glob of that tree). +- **Optional `--prune-stale`** deletes previously generated files this run did not emit (suffix + + `AUTO-GENERATED` banner; requires `.isaac_teleop_tracker_codegen`). CMake configure passes this + flag so renamed or removed trackers cannot leave an old header satisfying a stale `#include` on + the generated include path. Omit the flag for manual/dry runs that should only rewrite. +- **Prefer a manifest key over a template branch.** If one tracker differs, give it an override key; + add a new manifest `shape` only when the control flow genuinely differs. A shape that makes the + templates hard to follow is a signal to leave that tracker hand-written and say so in + [`../deviceio_trackers/AGENTS.md`](../deviceio_trackers/AGENTS.md). +- **`direction` must be `in` or `out`** — validated before defaults merge so a typo cannot silently + pick up the wrong overlay. **`shape=single_collection` requires `record=true`** for `direction=in`: + those templates always emit MCAP channel/viewer constructors, while the factory only omits them + when `record=false`. Non-recording multi-sample readers (e.g. `HapticCommandReaderTracker`) stay + hand-written — there is no generated `multi_endpoint` shape. +- **Emit code that already satisfies the layering rules** of the package it lands in — generated + `deviceio_trackers` facades must not include OpenXR headers, generated live impls own the + `SchemaTracker`, and so on. The generator is not exempt from a package's `AGENTS.md`. + +## Working on the templates + +- **Per-tracker C++ bodies** live under [`templates/`](templates/) as `*.hpp.template` / + `*.cpp.template` (`in/` and `out/`) plus `*.template` fragments under `fragments/` for + factory/pybind/traits. Direction-specific fragments use `_in` / `_out` suffixes (e.g. + `pybind_in.template`, `live_factory_out.template`, `recording_traits_in.template`); shared + pieces are `live_try_create.template` and `replay_try_create.template`. Edit those for structure and includes; use `@KEY@` placeholders filled by + `in_renderer.template_values()`. Template files use the `.template` suffix so + [`../../../cmake/ClangFormat.cmake`](../../../cmake/ClangFormat.cmake) does not format them + (only `*.hpp` / `*.cpp` are globbed), and [`templates/.clang-format`](templates/.clang-format) + sets `DisableFormat: true` so editors that map `*.cpp.template` to C++ for highlighting cannot + mangle `@KEY@` on save. Conditionals stay in Python (pick which template to load) + or as `#if @FLAG@` for optional fragments within one shape (e.g. `@HAS_TENSOR_IDENTIFIER@`). Do + **not** merge `out/` with `in/` via shared `#if`/`#else` — those control flows are different + enough that separate templates stay clearer. `*.hpp.template` / `*.cpp.template` carry SPDX + headers for REUSE; `in_renderer` strips them before prepending the emit-time COPYRIGHT / + AUTO-GENERATED banner. +- **One-line aggregators** (includes, dispatch rows, factory decls, cmake source lists, Python + `__all__`) remain `yield` loops in `generate_trackers.py` joined with `"\n"`. +- Generated C++ is **not** covered by the format gate (`cmake/ClangFormat.cmake` globs only + `src/` and `examples/` and excludes `build/`), so it does not need to be clang-format clean — but + it must compile. Read the emitted file under `${CMAKE_BINARY_DIR}/generated/trackers/` when a + template change fails to build; the compiler error points at generated text, not at the f-string. +- Generated `#include`s must be written as the **consumer** sees them (``, + ``), not as the in-tree relative paths the hand-written files + use, because generated sources sit in a different directory. +- Run `python test_manifest.py` after touching resolution rules, and + `generate_trackers.py --print-resolved` to see the fully expanded manifest before blaming a + template. +- A **full build is the only real test** of a template change: it is what proves the emitted + facade, impls, and every `.inc` fragment still agree with the hand-written files including them. diff --git a/src/core/codegen/generate_trackers.py b/src/core/codegen/generate_trackers.py new file mode 100644 index 000000000..23369a5ee --- /dev/null +++ b/src/core/codegen/generate_trackers.py @@ -0,0 +1,428 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Generate passthrough tracker C++ sources and CMake fragments from TOML manifests.""" + +from __future__ import annotations + +import argparse +import sys +from collections.abc import Iterator +from pathlib import Path +from typing import Any + +from in_renderer import render_template, render_with_copyright, template_values +from manifest import load_manifest +from templates import TrackerGenContext, enrich_context + +COPYRIGHT = ( + "// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n" + "// " + "SPDX-License-Identifier: Apache-2.0\n\n" + "// AUTO-GENERATED by src/core/codegen/generate_trackers.py — do not edit.\n\n" +) + + +def _write_if_changed(path: Path, content: str) -> bool: + """Write ``content`` to ``path`` only when it differs from the existing file. + + Returns True if the file was created or updated, False if left unchanged (mtime preserved). + """ + path.parent.mkdir(parents=True, exist_ok=True) + if path.is_file(): + try: + if path.read_text(encoding="utf-8") == content: + return False + except OSError: + # Unreadable existing file — fall through and overwrite. + pass + path.write_text(content, encoding="utf-8") + return True + + +def _join_lines(lines: Iterator[str]) -> str: + return "\n".join(lines) + "\n" + + +def _render_tracker_set(ctx: TrackerGenContext, prefix: str) -> dict[str, str]: + """Seven-file bundle: deviceio_base, facade hpp/cpp, live hpp/cpp, replay hpp/cpp.""" + return { + f"deviceio_base/{ctx.base_header}.hpp": render_with_copyright( + f"{prefix}/deviceio_base.hpp.template", ctx, COPYRIGHT + ), + f"deviceio_trackers/inc/deviceio_trackers/{ctx.header}.hpp": render_with_copyright( + f"{prefix}/facade.hpp.template", ctx, COPYRIGHT + ), + f"deviceio_trackers/{ctx.header}.cpp": render_with_copyright( + f"{prefix}/facade.cpp.template", ctx, COPYRIGHT + ), + f"live_trackers/{ctx.live_impl_file}.hpp": render_with_copyright( + f"{prefix}/live.hpp.template", ctx, COPYRIGHT + ), + f"live_trackers/{ctx.live_impl_file}.cpp": render_with_copyright( + f"{prefix}/live.cpp.template", ctx, COPYRIGHT + ), + f"replay_trackers/{ctx.replay_impl_file}.hpp": render_with_copyright( + f"{prefix}/replay.hpp.template", ctx, COPYRIGHT + ), + f"replay_trackers/{ctx.replay_impl_file}.cpp": render_with_copyright( + f"{prefix}/replay.cpp.template", ctx, COPYRIGHT + ), + } + + +def render_in(ctx: TrackerGenContext) -> dict[str, str]: + return _render_tracker_set(ctx, "in") + + +def render_out(ctx: TrackerGenContext) -> dict[str, str]: + return _render_tracker_set(ctx, "out") + + +def render_for_entry(ctx: TrackerGenContext) -> dict[str, str]: + if ctx.direction == "out": + return render_out(ctx) + if ctx.shape != "single_collection": + raise ValueError(f"tracker '{ctx.name}': unsupported shape '{ctx.shape}'") + return render_in(ctx) + + +def _render_fragment(name: str, ctx: TrackerGenContext) -> str: + return render_template(f"fragments/{name}", template_values(ctx)).rstrip("\n") + + +def _gen_live_includes(entries: list[dict[str, Any]]) -> Iterator[str]: + for entry in entries: + ctx = enrich_context(entry) + yield f'#include "{ctx.live_impl_file}.hpp"' + yield f"#include " + + +def _gen_replay_includes(entries: list[dict[str, Any]]) -> Iterator[str]: + for entry in entries: + ctx = enrich_context(entry) + yield f'#include "{ctx.replay_impl_file}.hpp"' + yield f"#include " + + +def _gen_tracker_forward_decls(entries: list[dict[str, Any]]) -> Iterator[str]: + for entry in entries: + ctx = enrich_context(entry) + yield f"class {ctx.cls};" + yield f"class {ctx.iface};" + + +def _gen_live_try_create(entries: list[dict[str, Any]]) -> Iterator[str]: + for index, entry in enumerate(entries): + if index > 0: + yield "" + yield _render_fragment("live_try_create.template", enrich_context(entry)) + + +def _gen_replay_try_create(entries: list[dict[str, Any]]) -> Iterator[str]: + for index, entry in enumerate(entries): + if index > 0: + yield "" + yield _render_fragment("replay_try_create.template", enrich_context(entry)) + + +def _gen_live_dispatch_rows(entries: list[dict[str, Any]]) -> Iterator[str]: + for entry in entries: + ctx = enrich_context(entry) + yield f" make_dispatch_entry<{ctx.cls}, {ctx.live_impl}>(&try_create_{ctx.name}_impl)," + + +def _gen_replay_dispatch_rows(entries: list[dict[str, Any]]) -> Iterator[str]: + for entry in entries: + ctx = enrich_context(entry) + yield f" &try_create_{ctx.name}_impl," + + +def _gen_factory_decls(entries: list[dict[str, Any]]) -> Iterator[str]: + for entry in entries: + ctx = enrich_context(entry) + yield ( + f" std::unique_ptr<{ctx.iface}> create_{ctx.name}_tracker_impl" + f"(const {ctx.cls}* tracker);" + ) + + +def _live_factory_template(ctx: TrackerGenContext) -> str: + if ctx.direction == "out": + return "live_factory_out.template" + return "live_factory_in.template" + + +def _replay_factory_template(ctx: TrackerGenContext) -> str: + if ctx.direction == "out": + return "replay_factory_out.template" + if ctx.record: + return "replay_factory_in.template" + return "replay_factory_in_plain.template" + + +def _gen_all_live_factory_methods(entries: list[dict[str, Any]]) -> Iterator[str]: + for index, entry in enumerate(entries): + if index > 0: + yield "" + ctx = enrich_context(entry) + yield _render_fragment(_live_factory_template(ctx), ctx) + + +def _gen_all_replay_factory_methods(entries: list[dict[str, Any]]) -> Iterator[str]: + for index, entry in enumerate(entries): + if index > 0: + yield "" + ctx = enrich_context(entry) + yield _render_fragment(_replay_factory_template(ctx), ctx) + + +def _gen_recording_traits(ctx: TrackerGenContext) -> Iterator[str]: + if ctx.direction != "in" or not ctx.record: + return + if ctx.mcap_channels is None or ctx.replay_channels is None: + raise ValueError(f"tracker '{ctx.name}': missing mcap/replay channels") + yield from _render_fragment("recording_traits_in.template", ctx).splitlines() + + +def _gen_all_recording_traits(entries: list[dict[str, Any]]) -> Iterator[str]: + first = True + for entry in entries: + block = list(_gen_recording_traits(enrich_context(entry))) + if not block: + continue + if not first: + yield "" + yield from block + first = False + + +def _pybind_template(ctx: TrackerGenContext) -> str: + if ctx.direction == "out": + return "pybind_out.template" + return "pybind_in.template" + + +def _gen_pybind_blocks(entries: list[dict[str, Any]]) -> Iterator[str]: + for entry in entries: + ctx = enrich_context(entry) + yield _render_fragment(_pybind_template(ctx), ctx) + + +def _gen_tracker_binding_includes(entries: list[dict[str, Any]]) -> Iterator[str]: + for entry in entries: + ctx = enrich_context(entry) + yield f"#include " + + +def _gen_python_tracker_exports(entries: list[dict[str, Any]]) -> Iterator[str]: + yield "# AUTO-GENERATED" + contexts = [enrich_context(entry) for entry in entries] + for ctx in contexts: + yield f"from ._deviceio_trackers import {ctx.cls}" + yield "" + yield "__all__ = [" + for ctx in contexts: + yield f' "{ctx.cls}",' + yield "]" + + +def emit_inc_fragments(entries: list[dict[str, Any]]) -> dict[str, str]: + return { + "inc/generated_live_includes.inc": _join_lines(_gen_live_includes(entries)), + "inc/generated_tracker_forward_decls.inc": _join_lines( + _gen_tracker_forward_decls(entries) + ), + "inc/generated_live_try_create.inc": _join_lines(_gen_live_try_create(entries)), + "inc/generated_live_dispatch_rows.inc": _join_lines( + _gen_live_dispatch_rows(entries) + ), + "inc/generated_live_factory_declarations.inc": _join_lines( + _gen_factory_decls(entries) + ), + "inc/generated_live_factory_methods.inc": _join_lines( + _gen_all_live_factory_methods(entries) + ), + "inc/generated_replay_includes.inc": _join_lines(_gen_replay_includes(entries)), + "inc/generated_replay_try_create.inc": _join_lines( + _gen_replay_try_create(entries) + ), + "inc/generated_replay_dispatch_rows.inc": _join_lines( + _gen_replay_dispatch_rows(entries) + ), + "inc/generated_replay_factory_declarations.inc": _join_lines( + _gen_factory_decls(entries) + ), + "inc/generated_replay_factory_methods.inc": _join_lines( + _gen_all_replay_factory_methods(entries) + ), + "inc/generated_recording_traits.inc": _join_lines( + _gen_all_recording_traits(entries) + ), + "inc/generated_tracker_binding_includes.inc": _join_lines( + _gen_tracker_binding_includes(entries) + ), + "inc/generated_tracker_bindings.inc": _join_lines(_gen_pybind_blocks(entries)), + "python/_generated_tracker_exports.py": _join_lines( + _gen_python_tracker_exports(entries) + ), + } + + +def _gen_cmake_source_list(name: str, items: list[str]) -> Iterator[str]: + yield f"set({name}" + for item in items: + yield f' "${{GENERATED_TRACKER_DIR}}/{item}"' + yield ")" + + +def _gen_cmake_content(sources: list[str], out_dir: Path) -> Iterator[str]: + facade: list[str] = [] + live: list[str] = [] + replay: list[str] = [] + for src in sources: + if src.startswith("deviceio_trackers/"): + facade.append(src) + elif src.startswith("live_trackers/"): + live.append(src) + elif src.startswith("replay_trackers/"): + replay.append(src) + + yield "# AUTO-GENERATED by generate_trackers.py" + yield f'set(GENERATED_TRACKER_DIR "{out_dir}")' + yield from _gen_cmake_source_list("GENERATED_TRACKER_FACADE_SOURCES", facade) + yield from _gen_cmake_source_list("GENERATED_TRACKER_LIVE_SOURCES", live) + yield from _gen_cmake_source_list("GENERATED_TRACKER_REPLAY_SOURCES", replay) + yield "set(GENERATED_TRACKER_DEVICEIO_BASE_INC_DIR ${GENERATED_TRACKER_DIR})" + yield "set(GENERATED_TRACKER_DEVICEIO_TRACKERS_INC_DIR ${GENERATED_TRACKER_DIR}/deviceio_trackers/inc)" + yield "set(GENERATED_TRACKER_LIVE_IMPL_INC_DIR ${GENERATED_TRACKER_DIR}/live_trackers)" + yield "set(GENERATED_TRACKER_REPLAY_IMPL_INC_DIR ${GENERATED_TRACKER_DIR}/replay_trackers)" + yield "set(GENERATED_TRACKER_INC_DIR ${GENERATED_TRACKER_DIR}/inc)" + + +def emit_cmake(sources: list[str], out_dir: Path) -> str: + return _join_lines(_gen_cmake_content(sources, out_dir)) + + +OWNERSHIP_MARKER = ".isaac_teleop_tracker_codegen" +_PRUNE_SUFFIXES = frozenset({".hpp", ".cpp", ".inc", ".py", ".cmake"}) +_AUTO_GENERATED_HEAD = b"AUTO-GENERATED" +_PRUNE_HEAD_BYTES = 512 + + +def _write_ownership_marker(out_dir: Path) -> Path: + marker = out_dir / OWNERSHIP_MARKER + content = "isaac_teleop tracker codegen\n" + _write_if_changed(marker, content) + return marker + + +def _is_generator_owned_file(path: Path) -> bool: + if path.name == OWNERSHIP_MARKER: + return False + if path.suffix not in _PRUNE_SUFFIXES: + return False + try: + head = path.read_bytes()[:_PRUNE_HEAD_BYTES] + except OSError: + return False + return _AUTO_GENERATED_HEAD in head + + +def _prune_stale(out_dir: Path, keep: set[Path]) -> None: + """Delete previously generated files this run did not emit (opt-in via ``--prune-stale``). + + Why: generated include dirs sit on the compiler search path. After a rename or removal, + an old header can keep satisfying a stale ``#include`` instead of failing the build. + Pruning removes those orphans so outdated includes break loudly. + + Only runs when ``OWNERSHIP_MARKER`` is present. Unlinks only files with a known generated + suffix and an AUTO-GENERATED banner in the first 512 bytes. + """ + if not out_dir.is_dir(): + return + if not (out_dir / OWNERSHIP_MARKER).is_file(): + return + for path in out_dir.rglob("*"): + if not path.is_file(): + continue + if path.resolve() in keep: + continue + if not _is_generator_owned_file(path): + continue + path.unlink() + + +def generate( + manifest_path: Path, + defaults_path: Path, + out_dir: Path, + cmake_path: Path, + *, + prune_stale: bool = False, +) -> None: + entries = load_manifest(manifest_path, defaults_path) + all_sources: list[str] = [] + written: set[Path] = set() + for entry in entries: + ctx = enrich_context(entry) + files = render_for_entry(ctx) + for rel, content in files.items(): + _write_if_changed(out_dir / rel, content) + written.add((out_dir / rel).resolve()) + if rel.endswith((".cpp", ".hpp")): + all_sources.append(rel) + + inc_files = emit_inc_fragments(entries) + for rel, content in inc_files.items(): + _write_if_changed(out_dir / rel, content) + written.add((out_dir / rel).resolve()) + + _write_if_changed( + cmake_path, emit_cmake(sorted(set(all_sources)), out_dir.resolve()) + ) + written.add(cmake_path.resolve()) + + marker = _write_ownership_marker(out_dir) + written.add(marker.resolve()) + + if prune_stale: + _prune_stale(out_dir, written) + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--manifest", type=Path, required=True) + parser.add_argument("--defaults", type=Path, required=True) + parser.add_argument("--out-dir", type=Path, required=True) + parser.add_argument("--emit-cmake", type=Path, required=True) + parser.add_argument( + "--prune-stale", + action="store_true", + help=( + "Delete previously generated files under --out-dir that this run did not emit " + "(so renamed trackers cannot leave old headers satisfying stale #includes). " + "CMake configure passes this flag; omit it for dry/manual runs that only rewrite." + ), + ) + parser.add_argument("--print-resolved", action="store_true") + args = parser.parse_args(argv) + + if args.print_resolved: + from manifest import print_resolved + + print_resolved(args.manifest, args.defaults) + return 0 + + generate( + args.manifest, + args.defaults, + args.out_dir, + args.emit_cmake, + prune_stale=args.prune_stale, + ) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/core/codegen/in_renderer.py b/src/core/codegen/in_renderer.py new file mode 100644 index 000000000..db4318608 --- /dev/null +++ b/src/core/codegen/in_renderer.py @@ -0,0 +1,77 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Load `templates/` bodies and substitute @KEY@ placeholders (stdlib-only).""" + +from __future__ import annotations + +from pathlib import Path + +from templates import TrackerGenContext + +TEMPLATE_ROOT = Path(__file__).resolve().parent / "templates" + + +def _strip_spdx_header(text: str) -> str: + """Drop leading // SPDX-* lines (and the blank line after) so emit-time COPYRIGHT is sole banner.""" + lines = text.splitlines(keepends=True) + i = 0 + while i < len(lines) and lines[i].startswith("// SPDX-"): + i += 1 + if i > 0 and i < len(lines) and lines[i].strip() == "": + i += 1 + return "".join(lines[i:]) + + +def render_template(rel_path: str, values: dict[str, str]) -> str: + text = _strip_spdx_header((TEMPLATE_ROOT / rel_path).read_text(encoding="utf-8")) + # Longer keys first so @NAME@ does not corrupt @SCHEMA_NAME@ / @LIVE_IMPL_FILE@ / etc. + for key, value in sorted(values.items(), key=lambda kv: len(kv[0]), reverse=True): + text = text.replace(f"@{key}@", value) + return text + + +def _tensor_identifier_expr(ctx: TrackerGenContext) -> str: + if ctx.facade_tensor_constant: + return f"std::string({ctx.cls}::TENSOR_IDENTIFIER)" + return f'"{ctx.tensor_identifier}"' + + +def template_values(ctx: TrackerGenContext) -> dict[str, str]: + """Flat @KEY@ map for per-tracker C++ templates.""" + values: dict[str, str] = { + "CLASS": ctx.cls, + "IFACE": ctx.iface, + "NAME": ctx.name, + "HEADER": ctx.header, + "BASE_HEADER": ctx.base_header, + "SCHEMA": ctx.schema, + "TRACKED_TYPE": ctx.tracked_type, + "DATA_TYPE": ctx.data_type, + "RECORD_TYPE": ctx.record_type, + "FB_TABLE": ctx.fb_table, + "LIVE_IMPL": ctx.live_impl, + "REPLAY_IMPL": ctx.replay_impl, + "LIVE_IMPL_FILE": ctx.live_impl_file, + "REPLAY_IMPL_FILE": ctx.replay_impl_file, + "MCAP_CHANNELS_TYPE": ctx.mcap_channels_type, + "SCHEMA_TRACKER_TYPE": ctx.schema_tracker_type, + "MCAP_VIEWERS_TYPE": ctx.mcap_viewers_type, + "TRAITS": ctx.traits, + "MAX_FLATBUFFER_SIZE": str(ctx.max_flatbuffer_size), + "TENSOR_IDENTIFIER": ctx.tensor_identifier, + "LOCALIZED_NAME": ctx.localized_name, + "PYTHON_ACCESSOR": ctx.python_accessor, + "SCHEMA_NAME": ctx.schema_name, + "TENSOR_IDENTIFIER_EXPR": _tensor_identifier_expr(ctx), + "HAS_TENSOR_IDENTIFIER": "1" if ctx.facade_tensor_constant else "0", + } + if ctx.mcap_channels is not None: + values["RECORDING_CHANNELS"] = ", ".join(f'"{c}"' for c in ctx.mcap_channels) + if ctx.replay_channels is not None: + values["REPLAY_CHANNELS"] = ", ".join(f'"{c}"' for c in ctx.replay_channels) + return values + + +def render_with_copyright(rel_path: str, ctx: TrackerGenContext, copyright: str) -> str: + return copyright + render_template(rel_path, template_values(ctx)) diff --git a/src/core/codegen/manifest.py b/src/core/codegen/manifest.py new file mode 100644 index 000000000..5b9f0d82a --- /dev/null +++ b/src/core/codegen/manifest.py @@ -0,0 +1,238 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Resolve tracker manifest entries with defaults.toml placeholder expansion.""" + +from __future__ import annotations + +import json +import re +import sys +import tomllib +from pathlib import Path +from typing import Any + +REQUIRED_KEYS = frozenset({"name", "table"}) +VALID_DIRECTIONS = frozenset({"in", "out"}) + +# Keys that must be present after resolution for code generation. +RESOLVED_REQUIRED = frozenset( + { + "name", + "direction", + "shape", + "schema", + "table", + "class", + "tensor_identifier", + "localized_name", + "channel", + "schema_name", + "traits", + "python_accessor", + "max_flatbuffer_size", + "record", + "facade_tensor_constant", + } +) + +_PLACEHOLDER = re.compile( + r"%(?P[a-zA-Z0-9_]+?)(?P_(?:CamelCase|UPPER))?%|(?P%%)" +) + + +def snake_to_camel(value: str) -> str: + """Convert snake_case to CamelCase, capitalizing after digits (3axis -> 3Axis).""" + parts = value.split("_") + out: list[str] = [] + for part in parts: + if not part: + continue + if part[0].isdigit(): + # Capitalize first alpha after leading digits. + idx = 0 + while idx < len(part) and part[idx].isdigit(): + idx += 1 + if idx < len(part): + out.append(part[:idx] + part[idx].upper() + part[idx + 1 :]) + else: + out.append(part) + else: + out.append(part[0].upper() + part[1:]) + return "".join(out) + + +def apply_transform(value: str, transform: str | None) -> str: + if transform == "_CamelCase": + return snake_to_camel(value) + if transform == "_UPPER": + return value.upper() + return value + + +def expand_string(template: str, values: dict[str, Any]) -> str: + def repl(match: re.Match[str]) -> str: + if match.group("literal"): + return "%" + key = match.group("key") + transform = match.group("transform") + if key not in values: + raise KeyError(key) + raw = values[key] + if isinstance(raw, (list, dict, bool, int, float)): + raise TypeError( + f"placeholder %{key}% requires a string value, got {type(raw).__name__}" + ) + return apply_transform(str(raw), transform) + + return _PLACEHOLDER.sub(repl, template) + + +def _expand_value(value: Any, values: dict[str, Any]) -> Any: + if isinstance(value, str): + return expand_string(value, values) + if isinstance(value, list): + return [_expand_value(item, values) for item in value] + if isinstance(value, dict): + return {k: _expand_value(v, values) for k, v in value.items()} + return value + + +def _merge_defaults( + base: dict[str, Any], + direction: str, + defaults_doc: dict[str, Any], +) -> dict[str, Any]: + root = dict(defaults_doc.get("defaults", {})) + overlay = root.pop(direction, {}) + if not isinstance(overlay, dict): + overlay = {} + for other_direction in VALID_DIRECTIONS: + root.pop(other_direction, None) + merged = root + merged.update(overlay) + merged.update(base) + return merged + + +def resolve_tracker_entry( + raw: dict[str, Any], + defaults_doc: dict[str, Any], +) -> dict[str, Any]: + if "name" not in raw: + raise ValueError("tracker entry missing required key 'name'") + if "table" not in raw: + raise ValueError(f"tracker '{raw['name']}': missing required key 'table'") + + direction = str( + raw.get("direction", defaults_doc.get("defaults", {}).get("direction", "in")) + ) + if direction not in VALID_DIRECTIONS: + raise ValueError( + f"tracker '{raw['name']}': invalid direction '{direction}' " + f"(expected one of: {', '.join(sorted(VALID_DIRECTIONS))})" + ) + working = _merge_defaults(raw, direction, defaults_doc) + + resolved: dict[str, Any] = {"name": raw["name"], "table": raw["table"]} + pending = {k: v for k, v in working.items() if k not in ("name", "table")} + + # Seed with explicit name/table so %name% / %table% work immediately. + resolved["name"] = raw["name"] + resolved["table"] = raw["table"] + + max_passes = len(pending) + 8 + for _ in range(max_passes): + progressed = False + for key in list(pending.keys()): + value = pending[key] + try: + new_value = _expand_value(value, resolved) + except KeyError: + continue + resolved[key] = new_value + del pending[key] + progressed = True + if not pending: + break + if not progressed: + missing = ", ".join(sorted(pending)) + raise ValueError( + f"tracker '{raw['name']}': could not resolve keys (cycle or missing reference): {missing}" + ) + + missing_required = sorted(RESOLVED_REQUIRED - resolved.keys()) + if missing_required: + raise ValueError( + f"tracker '{raw['name']}': missing required keys after resolution: {', '.join(missing_required)}" + ) + + record = resolved["record"] + if not isinstance(record, bool): + raise ValueError( + f"tracker '{raw['name']}': record must be a boolean, got {type(record).__name__}" + ) + + if resolved["direction"] == "in" and record: + for key in ("mcap_channels", "replay_channels"): + if key not in resolved: + raise ValueError( + f"tracker '{raw['name']}': missing required key '{key}' for direction=in" + ) + + # single_collection templates always emit MCAP channel/viewer constructors; the + # factory only omits them when record=false. Reject the mismatch rather than + # generating sources that cannot link against the factory methods. + if ( + resolved["direction"] == "in" + and resolved.get("shape") == "single_collection" + and not record + ): + raise ValueError( + f"tracker '{raw['name']}': shape=single_collection requires record=true " + "(non-recording multi-sample readers such as HapticCommandReaderTracker stay hand-written)" + ) + + return resolved + + +def load_defaults(path: Path) -> dict[str, Any]: + with path.open("rb") as handle: + return tomllib.load(handle) + + +def load_manifest(manifest_path: Path, defaults_path: Path) -> list[dict[str, Any]]: + defaults_doc = load_defaults(defaults_path) + with manifest_path.open("rb") as handle: + manifest = tomllib.load(handle) + entries = manifest.get("tracker", []) + if not isinstance(entries, list): + raise ValueError("trackers.toml: 'tracker' must be an array") + return [resolve_tracker_entry(entry, defaults_doc) for entry in entries] + + +def print_resolved(manifest_path: Path, defaults_path: Path) -> None: + for entry in load_manifest(manifest_path, defaults_path): + print(json.dumps(entry, indent=2, sort_keys=True)) + print() + + +def main(argv: list[str] | None = None) -> int: + import argparse + + parser = argparse.ArgumentParser(description="Resolve tracker manifest entries") + parser.add_argument("--manifest", type=Path, required=True) + parser.add_argument("--defaults", type=Path, required=True) + parser.add_argument("--print-resolved", action="store_true") + args = parser.parse_args(argv) + + if args.print_resolved: + print_resolved(args.manifest, args.defaults) + return 0 + + load_manifest(args.manifest, args.defaults) + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/core/codegen/templates.py b/src/core/codegen/templates.py new file mode 100644 index 000000000..839922fa0 --- /dev/null +++ b/src/core/codegen/templates.py @@ -0,0 +1,111 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""C++ and Python code templates for generated passthrough trackers.""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Any + + +def _header() -> str: + return ( + "// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.\n" + "// SPDX-License-Identifier: Apache-2.0\n" + "\n" + "// AUTO-GENERATED by src/core/codegen/generate_trackers.py — do not edit.\n" + "\n" + ) + + +@dataclass(frozen=True, slots=True) +class TrackerGenContext: + """Resolved manifest entry plus derived C++/file names for codegen.""" + + name: str + table: str + direction: str + shape: str + schema: str + cls: str + tensor_identifier: str + localized_name: str + channel: str + schema_name: str + traits: str + python_accessor: str + max_flatbuffer_size: int + record: bool + facade_tensor_constant: bool + mcap_channels: tuple[str, ...] | None + replay_channels: tuple[str, ...] | None + header: str + base_header: str + live_impl_file: str + replay_impl_file: str + iface: str + tracked_type: str + record_type: str + data_type: str + fb_table: str + mcap_channels_type: str + schema_tracker_type: str + mcap_viewers_type: str + live_impl: str + replay_impl: str + + +def enrich_context(entry: dict[str, Any]) -> TrackerGenContext: + cls = entry["class"] + table = entry["table"] + name = entry["name"] + if name.endswith("_tracker"): + header = name + base = f"{name}_base" + live_impl_file = f"live_{name}_impl" + replay_impl_file = f"replay_{name}_impl" + else: + header = f"{name}_tracker" + base = f"{name}_tracker_base" + live_impl_file = f"live_{name}_tracker_impl" + replay_impl_file = f"replay_{name}_tracker_impl" + + mcap_raw = entry.get("mcap_channels") + replay_raw = entry.get("replay_channels") + mcap_channels = tuple(mcap_raw) if mcap_raw is not None else None + replay_channels = tuple(replay_raw) if replay_raw is not None else None + + return TrackerGenContext( + name=name, + table=table, + direction=str(entry["direction"]), + shape=str(entry["shape"]), + schema=str(entry["schema"]), + cls=str(cls), + tensor_identifier=str(entry["tensor_identifier"]), + localized_name=str(entry["localized_name"]), + channel=str(entry["channel"]), + schema_name=str(entry["schema_name"]), + traits=str(entry["traits"]), + python_accessor=str(entry["python_accessor"]), + max_flatbuffer_size=int(entry["max_flatbuffer_size"]), + record=bool(entry["record"]), + facade_tensor_constant=bool(entry["facade_tensor_constant"]), + mcap_channels=mcap_channels, + replay_channels=replay_channels, + header=header, + base_header=base, + live_impl_file=live_impl_file, + replay_impl_file=replay_impl_file, + iface=f"I{cls}Impl", + tracked_type=f"{table}TrackedT", + record_type=f"{table}Record", + data_type=f"{table}T", + fb_table=table, + mcap_channels_type=f"{cls}McapChannels", + schema_tracker_type=f"{cls}SchemaTracker", + mcap_viewers_type=f"{cls}McapViewers", + live_impl=f"Live{cls}Impl", + replay_impl=f"Replay{cls}Impl", + ) diff --git a/src/core/codegen/templates/.clang-format b/src/core/codegen/templates/.clang-format new file mode 100644 index 000000000..66dbe40fc --- /dev/null +++ b/src/core/codegen/templates/.clang-format @@ -0,0 +1,10 @@ +--- +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Templates are associated with the C++ language in editors so they get syntax +# highlighting, which also makes format-on-save run clang-format on them. The +# @PLACEHOLDER@ markers are not valid C++ and get mangled ("@NAME@" becomes +# "@NAME @"), so formatting must be off for this whole subtree. +DisableFormat: true +SortIncludes: Never diff --git a/src/core/codegen/templates/fragments/live_factory_in.template b/src/core/codegen/templates/fragments/live_factory_in.template new file mode 100644 index 000000000..e9b8fb7eb --- /dev/null +++ b/src/core/codegen/templates/fragments/live_factory_in.template @@ -0,0 +1,8 @@ +std::unique_ptr<@IFACE@> LiveDeviceIOFactory::create_@NAME@_tracker_impl(const @CLASS@* tracker) +{ + std::unique_ptr<@MCAP_CHANNELS_TYPE@> channels; + if (should_record(tracker)) { + channels = @LIVE_IMPL@::create_mcap_channels(*writer_, get_name(tracker)); + } + return std::make_unique<@LIVE_IMPL@>(handles_, tracker, std::move(channels)); +} diff --git a/src/core/codegen/templates/fragments/live_factory_out.template b/src/core/codegen/templates/fragments/live_factory_out.template new file mode 100644 index 000000000..4c19c6c9a --- /dev/null +++ b/src/core/codegen/templates/fragments/live_factory_out.template @@ -0,0 +1,4 @@ +std::unique_ptr<@IFACE@> LiveDeviceIOFactory::create_@NAME@_tracker_impl(const @CLASS@* tracker) +{ + return std::make_unique<@LIVE_IMPL@>(handles_, tracker); +} diff --git a/src/core/codegen/templates/fragments/live_try_create.template b/src/core/codegen/templates/fragments/live_try_create.template new file mode 100644 index 000000000..c370a2b32 --- /dev/null +++ b/src/core/codegen/templates/fragments/live_try_create.template @@ -0,0 +1,5 @@ +std::unique_ptr try_create_@NAME@_impl(LiveDeviceIOFactory& factory, const ITracker& tracker) +{ + auto* typed = dynamic_cast(&tracker); + return typed ? factory.create_@NAME@_tracker_impl(typed) : nullptr; +} diff --git a/src/core/codegen/templates/fragments/pybind_in.template b/src/core/codegen/templates/fragments/pybind_in.template new file mode 100644 index 000000000..cb1b1cca7 --- /dev/null +++ b/src/core/codegen/templates/fragments/pybind_in.template @@ -0,0 +1,9 @@ + py::class_>(m, "@CLASS@") + .def(py::init(), py::arg("collection_id"), + py::arg("max_flatbuffer_size") = core::@CLASS@::DEFAULT_MAX_FLATBUFFER_SIZE) + .def( + "@PYTHON_ACCESSOR@", + [](const core::@CLASS@& self, const core::ITrackerSession& session) + { return share_tracked(self.get_data(session)); }, + py::arg("session"), + "Get the current tracked state (data is None when no data available)" TRACKED_LIFETIME_DOC); diff --git a/src/core/codegen/templates/fragments/pybind_out.template b/src/core/codegen/templates/fragments/pybind_out.template new file mode 100644 index 000000000..7ee50fbaa --- /dev/null +++ b/src/core/codegen/templates/fragments/pybind_out.template @@ -0,0 +1,12 @@ + py::class_> @NAME@_push(m, "@CLASS@"); + @NAME@_push.attr("DEFAULT_MAX_PAYLOAD_SIZE") = + static_cast(core::@CLASS@::DEFAULT_MAX_PAYLOAD_SIZE); + @NAME@_push + .def(py::init(), py::arg("collection_id"), py::arg("tensor_identifier"), + py::arg("max_payload_size") = core::@CLASS@::DEFAULT_MAX_PAYLOAD_SIZE) + .def( + "push", + [](const core::@CLASS@& self, const core::ITrackerSession& session, const core::@DATA_TYPE@& data) { + self.push(session, data); + }, + py::arg("session"), py::arg("data")); diff --git a/src/core/codegen/templates/fragments/recording_traits_in.template b/src/core/codegen/templates/fragments/recording_traits_in.template new file mode 100644 index 000000000..582f387db --- /dev/null +++ b/src/core/codegen/templates/fragments/recording_traits_in.template @@ -0,0 +1,6 @@ +struct @TRAITS@ +{ + static constexpr std::string_view schema_name = "@SCHEMA_NAME@"; + static constexpr std::array recording_channels = { @RECORDING_CHANNELS@ }; + static constexpr std::array replay_channels = { @REPLAY_CHANNELS@ }; +}; diff --git a/src/core/codegen/templates/fragments/replay_factory_in.template b/src/core/codegen/templates/fragments/replay_factory_in.template new file mode 100644 index 000000000..bc01f0cb4 --- /dev/null +++ b/src/core/codegen/templates/fragments/replay_factory_in.template @@ -0,0 +1,4 @@ +std::unique_ptr<@IFACE@> ReplayDeviceIOFactory::create_@NAME@_tracker_impl(const @CLASS@* tracker) +{ + return std::make_unique<@REPLAY_IMPL@>(open_reader(filename_), get_name(tracker)); +} diff --git a/src/core/codegen/templates/fragments/replay_factory_in_plain.template b/src/core/codegen/templates/fragments/replay_factory_in_plain.template new file mode 100644 index 000000000..7c2ce6d06 --- /dev/null +++ b/src/core/codegen/templates/fragments/replay_factory_in_plain.template @@ -0,0 +1,4 @@ +std::unique_ptr<@IFACE@> ReplayDeviceIOFactory::create_@NAME@_tracker_impl(const @CLASS@* /*tracker*/) +{ + return std::make_unique<@REPLAY_IMPL@>(); +} diff --git a/src/core/codegen/templates/fragments/replay_factory_out.template b/src/core/codegen/templates/fragments/replay_factory_out.template new file mode 100644 index 000000000..7c2ce6d06 --- /dev/null +++ b/src/core/codegen/templates/fragments/replay_factory_out.template @@ -0,0 +1,4 @@ +std::unique_ptr<@IFACE@> ReplayDeviceIOFactory::create_@NAME@_tracker_impl(const @CLASS@* /*tracker*/) +{ + return std::make_unique<@REPLAY_IMPL@>(); +} diff --git a/src/core/codegen/templates/fragments/replay_try_create.template b/src/core/codegen/templates/fragments/replay_try_create.template new file mode 100644 index 000000000..d2f1bef44 --- /dev/null +++ b/src/core/codegen/templates/fragments/replay_try_create.template @@ -0,0 +1,5 @@ +std::unique_ptr try_create_@NAME@_impl(ReplayDeviceIOFactory& factory, const ITracker& tracker) +{ + auto* typed = dynamic_cast(&tracker); + return typed ? factory.create_@NAME@_tracker_impl(typed) : nullptr; +} diff --git a/src/core/codegen/templates/in/deviceio_base.hpp.template b/src/core/codegen/templates/in/deviceio_base.hpp.template new file mode 100644 index 000000000..916ab653d --- /dev/null +++ b/src/core/codegen/templates/in/deviceio_base.hpp.template @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +#pragma once +#include +namespace core +{ +struct @TRACKED_TYPE@; +class @IFACE@ : public ITrackerImpl +{ +public: + virtual const @TRACKED_TYPE@& get_data() const = 0; +}; +} // namespace core diff --git a/src/core/codegen/templates/in/facade.cpp.template b/src/core/codegen/templates/in/facade.cpp.template new file mode 100644 index 000000000..2da87fb18 --- /dev/null +++ b/src/core/codegen/templates/in/facade.cpp.template @@ -0,0 +1,17 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include + +namespace core +{ +@CLASS@::@CLASS@(const std::string& collection_id, size_t max_flatbuffer_size) + : collection_id_(collection_id), max_flatbuffer_size_(max_flatbuffer_size) +{ + // No-op: members set in the initializer list. +} +const @TRACKED_TYPE@& @CLASS@::get_data(const ITrackerSession& session) const +{ + return static_cast(session.get_tracker_impl(*this)).get_data(); +} +} // namespace core diff --git a/src/core/codegen/templates/in/facade.hpp.template b/src/core/codegen/templates/in/facade.hpp.template new file mode 100644 index 000000000..38559fb48 --- /dev/null +++ b/src/core/codegen/templates/in/facade.hpp.template @@ -0,0 +1,42 @@ +// 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 +{ +class @CLASS@ : public ITracker +{ +public: + static constexpr size_t DEFAULT_MAX_FLATBUFFER_SIZE = @MAX_FLATBUFFER_SIZE@; + +#if @HAS_TENSOR_IDENTIFIER@ + static constexpr std::string_view TENSOR_IDENTIFIER = "@TENSOR_IDENTIFIER@"; +#endif + + explicit @CLASS@(const std::string& collection_id, size_t max_flatbuffer_size = DEFAULT_MAX_FLATBUFFER_SIZE); + std::string_view get_name() const override + { + return TRACKER_NAME; + } + const @TRACKED_TYPE@& get_data(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 = "@CLASS@"; + std::string collection_id_; + size_t max_flatbuffer_size_; +}; +} // namespace core diff --git a/src/core/codegen/templates/in/live.cpp.template b/src/core/codegen/templates/in/live.cpp.template new file mode 100644 index 000000000..d7e0a0177 --- /dev/null +++ b/src/core/codegen/templates/in/live.cpp.template @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "@LIVE_IMPL_FILE@.hpp" + +#include +#include + +namespace core +{ +namespace +{ +SchemaTrackerConfig make_@NAME@_tensor_config(const @CLASS@* tracker) +{ + SchemaTrackerConfig cfg; + cfg.collection_id = tracker->collection_id(); + cfg.max_flatbuffer_size = tracker->max_flatbuffer_size(); + cfg.tensor_identifier = @TENSOR_IDENTIFIER_EXPR@; + cfg.localized_name = "@LOCALIZED_NAME@"; + return cfg; +} +} // namespace +std::unique_ptr<@MCAP_CHANNELS_TYPE@> @LIVE_IMPL@::create_mcap_channels(mcap::McapWriter& writer, + std::string_view base_name) +{ + return std::make_unique<@MCAP_CHANNELS_TYPE@>( + writer, base_name, @TRAITS@::schema_name, + std::vector(@TRAITS@::recording_channels.begin(), @TRAITS@::recording_channels.end())); +} +@LIVE_IMPL@::@LIVE_IMPL@(const OpenXRSessionHandles& handles, + const @CLASS@* tracker, + std::unique_ptr<@MCAP_CHANNELS_TYPE@> mcap_channels) + : mcap_channels_(std::move(mcap_channels)), + m_schema_reader(handles, make_@NAME@_tensor_config(tracker), mcap_channels_.get(), 0, 1) +{ + // No-op: members set in the initializer list. +} +void @LIVE_IMPL@::update(int64_t /*monotonic_time_ns*/) +{ + m_schema_reader.update(m_tracked.data); +} +const @TRACKED_TYPE@& @LIVE_IMPL@::get_data() const +{ + return m_tracked; +} + +} // namespace core diff --git a/src/core/codegen/templates/in/live.hpp.template b/src/core/codegen/templates/in/live.hpp.template new file mode 100644 index 000000000..47632480f --- /dev/null +++ b/src/core/codegen/templates/in/live.hpp.template @@ -0,0 +1,38 @@ +// 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 +{ +using @MCAP_CHANNELS_TYPE@ = McapTrackerChannels<@RECORD_TYPE@, @FB_TABLE@>; +using @SCHEMA_TRACKER_TYPE@ = SchemaTracker<@RECORD_TYPE@, @FB_TABLE@>; +class @LIVE_IMPL@ : public @IFACE@ +{ +public: + static std::vector required_extensions() + { + return SchemaTrackerBase::get_required_extensions(); + } + static std::unique_ptr<@MCAP_CHANNELS_TYPE@> create_mcap_channels(mcap::McapWriter& writer, + std::string_view base_name); + @LIVE_IMPL@(const OpenXRSessionHandles& handles, + const @CLASS@* tracker, + std::unique_ptr<@MCAP_CHANNELS_TYPE@> mcap_channels); + void update(int64_t monotonic_time_ns) override; + const @TRACKED_TYPE@& get_data() const override; + +private: + std::unique_ptr<@MCAP_CHANNELS_TYPE@> mcap_channels_; + @SCHEMA_TRACKER_TYPE@ m_schema_reader; + @TRACKED_TYPE@ m_tracked; +}; +} // namespace core diff --git a/src/core/codegen/templates/in/replay.cpp.template b/src/core/codegen/templates/in/replay.cpp.template new file mode 100644 index 000000000..de1adf243 --- /dev/null +++ b/src/core/codegen/templates/in/replay.cpp.template @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "@REPLAY_IMPL_FILE@.hpp" + +#include +#include + +#include +#include +#include + +namespace core +{ +@REPLAY_IMPL@::@REPLAY_IMPL@(std::unique_ptr reader, std::string_view base_name) + : mcap_viewers_(std::make_unique<@MCAP_VIEWERS_TYPE@>( + std::move(reader), + base_name, + std::vector(@TRAITS@::replay_channels.begin(), @TRAITS@::replay_channels.end()))), + no_data_message_("@REPLAY_IMPL@[" + std::string(base_name) + "]: no data (EOF or gap)") +{ + // No-op: members set in the initializer list. +} +const @TRACKED_TYPE@& @REPLAY_IMPL@::get_data() const +{ + return tracked_; +} +void @REPLAY_IMPL@::update(int64_t /*monotonic_time_ns*/) +{ + auto record = mcap_viewers_->read(0); + if (record) + { + tracked_.data = std::move(record->data); + warned_no_data_ = false; + } + else + { + if (!warned_no_data_) + { + std::cerr << no_data_message_ << std::endl; + warned_no_data_ = true; + } + tracked_.data.reset(); + } +} +} // namespace core diff --git a/src/core/codegen/templates/in/replay.hpp.template b/src/core/codegen/templates/in/replay.hpp.template new file mode 100644 index 000000000..b56f4341a --- /dev/null +++ b/src/core/codegen/templates/in/replay.hpp.template @@ -0,0 +1,29 @@ +// 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 +namespace core +{ +using @MCAP_VIEWERS_TYPE@ = McapTrackerViewers<@RECORD_TYPE@>; +class @REPLAY_IMPL@ : public @IFACE@ +{ +public: + @REPLAY_IMPL@(std::unique_ptr reader, std::string_view base_name); + void update(int64_t monotonic_time_ns) override; + const @TRACKED_TYPE@& get_data() const override; + +private: + @TRACKED_TYPE@ tracked_; + std::unique_ptr<@MCAP_VIEWERS_TYPE@> mcap_viewers_; + std::string no_data_message_; + bool warned_no_data_ = false; +}; +} // namespace core diff --git a/src/core/codegen/templates/out/deviceio_base.hpp.template b/src/core/codegen/templates/out/deviceio_base.hpp.template new file mode 100644 index 000000000..a4435005c --- /dev/null +++ b/src/core/codegen/templates/out/deviceio_base.hpp.template @@ -0,0 +1,14 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +#pragma once +#include +namespace core +{ +struct @DATA_TYPE@; +class @IFACE@ : public ITrackerImpl +{ +public: + virtual void push(const @DATA_TYPE@& data) const = 0; +}; +} // namespace core diff --git a/src/core/codegen/templates/out/facade.cpp.template b/src/core/codegen/templates/out/facade.cpp.template new file mode 100644 index 000000000..d1eac8d0d --- /dev/null +++ b/src/core/codegen/templates/out/facade.cpp.template @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include + +namespace core +{ +@CLASS@::@CLASS@(std::string collection_id, std::string tensor_identifier, std::size_t max_payload_size) + : collection_id_(std::move(collection_id)), + tensor_identifier_(std::move(tensor_identifier)), + max_payload_size_(max_payload_size) +{ + // No-op: members set in the initializer list. +} +void @CLASS@::push(const ITrackerSession& session, const @DATA_TYPE@& data) const +{ + static_cast(session.get_tracker_impl(*this)).push(data); +} +} // namespace core diff --git a/src/core/codegen/templates/out/facade.hpp.template b/src/core/codegen/templates/out/facade.hpp.template new file mode 100644 index 000000000..f53eaf228 --- /dev/null +++ b/src/core/codegen/templates/out/facade.hpp.template @@ -0,0 +1,44 @@ +// 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 +{ +class @CLASS@ : public ITracker +{ +public: + static constexpr std::size_t DEFAULT_MAX_PAYLOAD_SIZE = @MAX_FLATBUFFER_SIZE@; + @CLASS@(std::string collection_id, + std::string tensor_identifier, + std::size_t max_payload_size = DEFAULT_MAX_PAYLOAD_SIZE); + std::string_view get_name() const override + { + return TRACKER_NAME; + } + void push(const ITrackerSession& session, const @DATA_TYPE@& data) const; + const std::string& collection_id() const + { + return collection_id_; + } + const std::string& tensor_identifier() const + { + return tensor_identifier_; + } + std::size_t max_payload_size() const + { + return max_payload_size_; + } + +private: + static constexpr const char* TRACKER_NAME = "@CLASS@"; + std::string collection_id_; + std::string tensor_identifier_; + std::size_t max_payload_size_; +}; +} // namespace core diff --git a/src/core/codegen/templates/out/live.cpp.template b/src/core/codegen/templates/out/live.cpp.template new file mode 100644 index 000000000..5db3aa062 --- /dev/null +++ b/src/core/codegen/templates/out/live.cpp.template @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "@LIVE_IMPL_FILE@.hpp" + +#include +#include + +namespace core +{ +namespace +{ +SchemaPusherConfig make_@NAME@_push_config(const @CLASS@* tracker) +{ + SchemaPusherConfig cfg; + cfg.collection_id = tracker->collection_id(); + cfg.max_flatbuffer_size = tracker->max_payload_size(); + cfg.tensor_identifier = tracker->tensor_identifier(); + cfg.localized_name = tracker->tensor_identifier(); + return cfg; +} +} // namespace +@LIVE_IMPL@::@LIVE_IMPL@(const OpenXRSessionHandles& handles, const @CLASS@* tracker) + : pusher_(handles, make_@NAME@_push_config(tracker)) +{ + // No-op: members set in the initializer list. +} +void @LIVE_IMPL@::update(int64_t monotonic_time_ns) +{ + last_update_time_ns_ = monotonic_time_ns; +} +void @LIVE_IMPL@::push(const @DATA_TYPE@& data) const +{ + flatbuffers::FlatBufferBuilder builder(pusher_.config().max_flatbuffer_size); + auto offset = @FB_TABLE@::Pack(builder, &data); + builder.Finish(offset); + const int64_t now_ns = last_update_time_ns_ > 0 ? last_update_time_ns_ : core::os_monotonic_now_ns(); + pusher_.push_buffer(builder.GetBufferPointer(), builder.GetSize(), now_ns, now_ns); +} +} // namespace core diff --git a/src/core/codegen/templates/out/live.hpp.template b/src/core/codegen/templates/out/live.hpp.template new file mode 100644 index 000000000..6d72b4be2 --- /dev/null +++ b/src/core/codegen/templates/out/live.hpp.template @@ -0,0 +1,32 @@ +// 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 + +namespace core +{ +class @LIVE_IMPL@ : public @IFACE@ +{ +public: + static std::vector required_extensions() + { + return SchemaPusher::get_required_extensions(); + } + @LIVE_IMPL@(const OpenXRSessionHandles& handles, const @CLASS@* tracker); + void update(int64_t monotonic_time_ns) override; + void push(const @DATA_TYPE@& data) const override; + +private: + mutable SchemaPusher pusher_; + int64_t last_update_time_ns_{ 0 }; +}; +} // namespace core diff --git a/src/core/codegen/templates/out/replay.cpp.template b/src/core/codegen/templates/out/replay.cpp.template new file mode 100644 index 000000000..b5e32ec7a --- /dev/null +++ b/src/core/codegen/templates/out/replay.cpp.template @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +#include "@REPLAY_IMPL_FILE@.hpp" + +namespace core +{ +void @REPLAY_IMPL@::update(int64_t /*monotonic_time_ns*/) +{ + // No-op: out-direction trackers have no replay stream. +} +void @REPLAY_IMPL@::push(const @DATA_TYPE@&) const +{ + // No-op: out-direction trackers have no replay stream. +} +} // namespace core diff --git a/src/core/codegen/templates/out/replay.hpp.template b/src/core/codegen/templates/out/replay.hpp.template new file mode 100644 index 000000000..23aa1cf68 --- /dev/null +++ b/src/core/codegen/templates/out/replay.hpp.template @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +#pragma once +#include +#include + +#include + +namespace core +{ +class @REPLAY_IMPL@ : public @IFACE@ +{ +public: + @REPLAY_IMPL@() = default; + void update(int64_t monotonic_time_ns) override; + void push(const @DATA_TYPE@&) const override; +}; +} // namespace core diff --git a/src/core/codegen/test_generate_prune.py b/src/core/codegen/test_generate_prune.py new file mode 100644 index 000000000..9fb848bf6 --- /dev/null +++ b/src/core/codegen/test_generate_prune.py @@ -0,0 +1,118 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Unit tests for generator output writes and stale-file pruning.""" + +from __future__ import annotations + +import tempfile +import time +import unittest +from pathlib import Path + +from generate_trackers import ( + _is_generator_owned_file, + _prune_stale, + _write_if_changed, + _write_ownership_marker, + generate, +) + + +class WriteIfChangedTest(unittest.TestCase): + def test_skips_write_when_content_matches(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + path = Path(tmp) / "out.hpp" + path.write_text("// AUTO-GENERATED\nsame\n", encoding="utf-8") + before = path.stat().st_mtime_ns + time.sleep(0.01) + changed = _write_if_changed(path, "// AUTO-GENERATED\nsame\n") + self.assertFalse(changed) + self.assertEqual(path.stat().st_mtime_ns, before) + + def test_writes_when_content_differs(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + path = Path(tmp) / "out.hpp" + path.write_text("old\n", encoding="utf-8") + before = path.stat().st_mtime_ns + time.sleep(0.01) + changed = _write_if_changed(path, "new\n") + self.assertTrue(changed) + self.assertEqual(path.read_text(encoding="utf-8"), "new\n") + self.assertGreater(path.stat().st_mtime_ns, before) + + +class PruneStaleTest(unittest.TestCase): + def test_no_prune_without_ownership_marker(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + out_dir = Path(tmp) + stale = out_dir / "stale.hpp" + stale.write_text("// AUTO-GENERATED\n", encoding="utf-8") + _prune_stale(out_dir, keep=set()) + self.assertTrue(stale.is_file()) + + def test_stale_generated_file_removed(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + out_dir = Path(tmp) + _write_ownership_marker(out_dir) + stale = out_dir / "stale.hpp" + stale.write_text( + "// AUTO-GENERATED by generate_trackers.py\n", encoding="utf-8" + ) + _prune_stale(out_dir, keep=set()) + self.assertFalse(stale.is_file()) + + def test_foreign_file_without_banner_survives(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + out_dir = Path(tmp) + _write_ownership_marker(out_dir) + foreign = out_dir / "hand_written.hpp" + foreign.write_text("#pragma once\n", encoding="utf-8") + _prune_stale(out_dir, keep=set()) + self.assertTrue(foreign.is_file()) + + def test_is_generator_owned_rejects_wrong_suffix(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + path = Path(tmp) / "notes.txt" + path.write_text("AUTO-GENERATED\n", encoding="utf-8") + self.assertFalse(_is_generator_owned_file(path)) + + def test_ownership_marker_never_pruned_as_generated(self) -> None: + with tempfile.TemporaryDirectory() as tmp: + out_dir = Path(tmp) + marker = _write_ownership_marker(out_dir) + self.assertFalse(_is_generator_owned_file(marker)) + + def test_generate_default_does_not_prune(self) -> None: + core = Path(__file__).resolve().parents[1] + manifest = core / "deviceio_trackers" / "trackers.toml" + defaults = core / "deviceio_trackers" / "defaults.toml" + if not manifest.is_file(): + self.skipTest("trackers.toml missing") + with tempfile.TemporaryDirectory() as tmp: + out_dir = Path(tmp) + orphan = out_dir / "deviceio_trackers" / "orphan_tracker.cpp" + orphan.parent.mkdir(parents=True) + orphan.write_text( + "// AUTO-GENERATED by generate_trackers.py\n", encoding="utf-8" + ) + generate( + manifest, + defaults, + out_dir, + out_dir / "generated_sources.cmake", + prune_stale=False, + ) + self.assertTrue(orphan.is_file()) + generate( + manifest, + defaults, + out_dir, + out_dir / "generated_sources.cmake", + prune_stale=True, + ) + self.assertFalse(orphan.is_file()) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/core/codegen/test_manifest.py b/src/core/codegen/test_manifest.py new file mode 100644 index 000000000..af245d159 --- /dev/null +++ b/src/core/codegen/test_manifest.py @@ -0,0 +1,177 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Unit tests for tracker manifest placeholder resolution.""" + +from __future__ import annotations + +import unittest +from pathlib import Path + +from manifest import load_defaults, load_manifest, resolve_tracker_entry, snake_to_camel + + +CORE_ROOT = Path(__file__).resolve().parents[1] +DEFAULTS_PATH = CORE_ROOT / "deviceio_trackers" / "defaults.toml" + + +class SnakeToCamelTest(unittest.TestCase): + def test_generic_pedal(self) -> None: + self.assertEqual(snake_to_camel("generic_3axis_pedal"), "Generic3AxisPedal") + + def test_se3(self) -> None: + self.assertEqual(snake_to_camel("se3_tracker"), "Se3Tracker") + + +class ManifestResolverTest(unittest.TestCase): + def test_cycle_is_rejected(self) -> None: + defaults = {"defaults": {"a": "%b%", "b": "%a%"}} + with self.assertRaises(ValueError) as ctx: + resolve_tracker_entry( + {"name": "x", "table": "T", "a": "%b%", "b": "%a%"}, defaults + ) + self.assertIn("cycle", str(ctx.exception).lower()) + + def test_missing_reference(self) -> None: + defaults = {"defaults": {"tensor_identifier": "%missing%"}} + with self.assertRaises(ValueError): + resolve_tracker_entry({"name": "x", "table": "T"}, defaults) + + def test_joint_state_defaults(self) -> None: + entry = resolve_tracker_entry( + { + "name": "joint_state", + "table": "JointStateOutput", + "max_flatbuffer_size": 4096, + }, + load_defaults(DEFAULTS_PATH), + ) + self.assertEqual(entry["class"], "JointStateTracker") + self.assertEqual(entry["schema"], "joint_state") + self.assertEqual(entry["traits"], "JointStateRecordingTraits") + self.assertEqual(entry["mcap_channels"], ["joint_state", "joint_state_tracked"]) + + def test_se3_class_override(self) -> None: + entry = resolve_tracker_entry( + { + "name": "se3_tracker", + "table": "Se3TrackerPose", + "class": "Se3Tracker", + "max_flatbuffer_size": 256, + }, + load_defaults(DEFAULTS_PATH), + ) + self.assertEqual(entry["class"], "Se3Tracker") + self.assertEqual(entry["tensor_identifier"], "se3_tracker") + + def test_pedal_schema_and_traits_override(self) -> None: + entry = resolve_tracker_entry( + { + "name": "generic_3axis_pedal", + "table": "Generic3AxisPedalOutput", + "schema": "pedals", + "channel": "pedals", + "traits": "PedalRecordingTraits", + "max_flatbuffer_size": 256, + "python_accessor": "get_pedal_data", + }, + load_defaults(DEFAULTS_PATH), + ) + self.assertEqual(entry["schema"], "pedals") + self.assertEqual(entry["traits"], "PedalRecordingTraits") + self.assertEqual(entry["python_accessor"], "get_pedal_data") + + def test_out_direction_defaults(self) -> None: + entry = resolve_tracker_entry( + { + "name": "haptic_command", + "direction": "out", + "table": "HapticCommand", + "tensor_identifier": "haptic_command", + }, + load_defaults(DEFAULTS_PATH), + ) + self.assertEqual(entry["class"], "HapticCommandPushTracker") + self.assertFalse(entry["record"]) + self.assertEqual(entry["python_accessor"], "push") + self.assertNotIn("in", entry) + self.assertNotIn("mcap_channels", entry) + + def test_in_direction_has_no_out_overlay_keys(self) -> None: + entry = resolve_tracker_entry( + {"name": "joint_state", "table": "JointStateOutput"}, + load_defaults(DEFAULTS_PATH), + ) + self.assertEqual(entry["direction"], "in") + self.assertNotIn("out", entry) + + def test_unknown_direction_is_rejected_before_defaults_merge(self) -> None: + # Only ``defaults.in`` carries required keys. Merging under direction="bogus" + # would either hang on missing placeholders or invent a silent overlay — so + # rejection must happen before ``_merge_defaults``. + defaults = { + "defaults": { + "direction": "in", + "in": { + "shape": "single_collection", + "schema": "%name%", + "class": "%name_CamelCase%Tracker", + "tensor_identifier": "%name%", + "localized_name": "%class%", + "channel": "%name%", + "schema_name": "core.%table%Record", + "traits": "%channel_CamelCase%RecordingTraits", + "python_accessor": "get_data", + "max_flatbuffer_size": 512, + "record": True, + "facade_tensor_constant": False, + "mcap_channels": ["x"], + "replay_channels": ["x"], + }, + } + } + with self.assertRaises(ValueError) as ctx: + resolve_tracker_entry( + {"name": "x", "table": "T", "direction": "bogus"}, defaults + ) + message = str(ctx.exception).lower() + self.assertIn("invalid direction", message) + self.assertIn("bogus", message) + # Must not fall through to placeholder / missing-key errors from a bad merge. + self.assertNotIn("could not resolve", message) + self.assertNotIn("missing required", message) + + def test_single_collection_without_record_is_rejected(self) -> None: + with self.assertRaises(ValueError) as ctx: + resolve_tracker_entry( + { + "name": "x", + "table": "T", + "shape": "single_collection", + "record": False, + }, + load_defaults(DEFAULTS_PATH), + ) + message = str(ctx.exception).lower() + self.assertIn("single_collection", message) + self.assertIn("record=true", message) + + def test_record_must_be_boolean(self) -> None: + with self.assertRaises(ValueError) as ctx: + resolve_tracker_entry( + {"name": "x", "table": "T", "record": "false"}, + load_defaults(DEFAULTS_PATH), + ) + message = str(ctx.exception).lower() + self.assertIn("record", message) + self.assertIn("boolean", message) + + def test_load_manifest_file(self) -> None: + manifest = CORE_ROOT / "deviceio_trackers" / "trackers.toml" + self.assertTrue(manifest.is_file(), f"missing required manifest: {manifest}") + entries = load_manifest(manifest, DEFAULTS_PATH) + self.assertGreaterEqual(len(entries), 1) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/core/deviceio_base/AGENTS.md b/src/core/deviceio_base/AGENTS.md index e3a92a338..a9b611ae9 100644 --- a/src/core/deviceio_base/AGENTS.md +++ b/src/core/deviceio_base/AGENTS.md @@ -12,9 +12,20 @@ SPDX-License-Identifier: Apache-2.0 - **`ITrackerImpl::update`** takes **`int64_t monotonic_time_ns`** (system monotonic clock, same domain as `core::os_monotonic_now_ns()`). - **Do not** use `XrTime`, ``, or OpenXR link targets in this library. Keep the tracker abstraction runtime-agnostic. +## Generated `ITrackerImpl` headers + +Only **hand-written** trackers keep their `_tracker_base.hpp` in `cpp/inc/deviceio_base/`. +The impl interface for a tracker declared in +[`../deviceio_trackers/trackers.toml`](../deviceio_trackers/trackers.toml) is generated into +`${CMAKE_BINARY_DIR}/generated/trackers/deviceio_base/` and reaches consumers through the same +`` include path, so a missing header there usually means a stale build tree +rather than a missing file. Do not hand-add a base header for a manifest tracker. + ## CMake - **`deviceio_base`** is an **INTERFACE** library: list only what the headers actually need (e.g. `isaacteleop_schema`). Do **not** link `OpenXR::headers` or `oxr::oxr_utils` here. +- The generated-header directory is added by the packages that consume it (`deviceio_trackers`, + `live_trackers`, `replay_trackers`), not by this INTERFACE target. ## Fallout for dependents diff --git a/src/core/deviceio_base/cpp/inc/deviceio_base/generic_3axis_pedal_tracker_base.hpp b/src/core/deviceio_base/cpp/inc/deviceio_base/generic_3axis_pedal_tracker_base.hpp deleted file mode 100644 index 9b9724a05..000000000 --- a/src/core/deviceio_base/cpp/inc/deviceio_base/generic_3axis_pedal_tracker_base.hpp +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -#pragma once - -#include "tracker.hpp" - -namespace core -{ - -struct Generic3AxisPedalOutputTrackedT; - -// Abstract base interface for Generic3AxisPedalTracker implementations. -class IGeneric3AxisPedalTrackerImpl : public ITrackerImpl -{ -public: - virtual const Generic3AxisPedalOutputTrackedT& get_data() const = 0; -}; - -} // namespace core diff --git a/src/core/deviceio_base/cpp/inc/deviceio_base/joint_state_tracker_base.hpp b/src/core/deviceio_base/cpp/inc/deviceio_base/joint_state_tracker_base.hpp deleted file mode 100644 index b9567e09f..000000000 --- a/src/core/deviceio_base/cpp/inc/deviceio_base/joint_state_tracker_base.hpp +++ /dev/null @@ -1,23 +0,0 @@ -// 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 JointStateOutputTrackedT; - -// Abstract base interface for JointStateTracker implementations. -// -// Backs a generic joint-space device (leader arm, exoskeleton, glove, ...): the implementation -// keeps the last-known JointStateOutput snapshot, which the JointStateTracker facade exposes. -class IJointStateTrackerImpl : public ITrackerImpl -{ -public: - virtual const JointStateOutputTrackedT& get_data() const = 0; -}; - -} // namespace core diff --git a/src/core/deviceio_base/cpp/inc/deviceio_base/oglo_tactile_tracker_base.hpp b/src/core/deviceio_base/cpp/inc/deviceio_base/oglo_tactile_tracker_base.hpp deleted file mode 100644 index 846010742..000000000 --- a/src/core/deviceio_base/cpp/inc/deviceio_base/oglo_tactile_tracker_base.hpp +++ /dev/null @@ -1,20 +0,0 @@ -// 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 OgloGloveSampleTrackedT; - -// Abstract base interface for OgloTactileTracker implementations. -class IOgloTactileTrackerImpl : public ITrackerImpl -{ -public: - virtual const OgloGloveSampleTrackedT& get_data() const = 0; -}; - -} // namespace core diff --git a/src/core/deviceio_base/cpp/inc/deviceio_base/se3_tracker_base.hpp b/src/core/deviceio_base/cpp/inc/deviceio_base/se3_tracker_base.hpp deleted file mode 100644 index 97054b067..000000000 --- a/src/core/deviceio_base/cpp/inc/deviceio_base/se3_tracker_base.hpp +++ /dev/null @@ -1,24 +0,0 @@ -// 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 Se3TrackerPoseTrackedT; - -// Abstract base interface for Se3Tracker implementations. -// -// Backs a generic SE3 (6-DoF pose) tracker device (tracker puck, mocap rigid body, logical -// tracker derived from another device, ...): the implementation keeps the last-known -// Se3TrackerPose snapshot, which the Se3Tracker facade exposes. -class ISe3TrackerImpl : public ITrackerImpl -{ -public: - virtual const Se3TrackerPoseTrackedT& get_data() const = 0; -}; - -} // namespace core diff --git a/src/core/deviceio_session/AGENTS.md b/src/core/deviceio_session/AGENTS.md index b59383d10..977088ad5 100644 --- a/src/core/deviceio_session/AGENTS.md +++ b/src/core/deviceio_session/AGENTS.md @@ -26,3 +26,4 @@ SPDX-License-Identifier: Apache-2.0 - Tracker interface contract: [`../deviceio_base/AGENTS.md`](../deviceio_base/AGENTS.md) - Live factory + impls: [`../live_trackers/AGENTS.md`](../live_trackers/AGENTS.md) +- Replay factory + impls: [`../replay_trackers/AGENTS.md`](../replay_trackers/AGENTS.md) diff --git a/src/core/deviceio_trackers/AGENTS.md b/src/core/deviceio_trackers/AGENTS.md index 64649df18..b2584aa1d 100644 --- a/src/core/deviceio_trackers/AGENTS.md +++ b/src/core/deviceio_trackers/AGENTS.md @@ -7,11 +7,47 @@ SPDX-License-Identifier: Apache-2.0 **CRITICAL (non-optional):** Before editing this package, complete the mandatory **`AGENTS.md` preflight** in [`../../../AGENTS.md`](../../../AGENTS.md) (read every applicable `AGENTS.md` on your paths, not just this file). +## Passthrough trackers come from the manifest — do not hand-write them + +A **passthrough** tracker is one whose live impl only moves a FlatBuffer table between a plugin's +`SchemaPusher` and the host (no `xrLocate*` calls, no custom connection state). Those are declared +in **[`trackers.toml`](trackers.toml)** and generated at configure time by +[`../codegen/generate_trackers.py`](../codegen/generate_trackers.py); the facade `.hpp`/`.cpp` land +under `${CMAKE_BINARY_DIR}/generated/trackers/`, **not** in `cpp/`. + +- **Adding one is a manifest edit**, not a new source file: `name` and `table` are required, every + other key has a `%placeholder%` default in [`defaults.toml`](defaults.toml). Override a key only + where the convention genuinely does not hold (`se3_tracker` sets `class`; `generic_3axis_pedal` + sets `schema` because its `.fbs` is `pedals.fbs`). +- **`direction = "out"`** generates a typed `PushTracker` (Teleop → plugin) instead of a + reader facade. `TensorPushTracker` stays as the untyped `bytes` escape hatch — keep it. +- **Do not hand-register a manifest tracker.** Factory dispatch rows, factory methods, pybind + blocks, MCAP recording traits, and the Python `__all__` are emitted as `.inc` fragments that the + hand-written files `#include`; adding a row yourself produces a duplicate definition. +- **`__init__.py` needs no edit for new manifest trackers.** + [`../../../python/isaacteleop/deviceio_trackers/__init__.py`](../../../python/isaacteleop/deviceio_trackers/__init__.py) + star-imports `_generated_tracker_exports` (staged from configure-time codegen into the wheel + tree; for scikit-build-core / editable installs it is also installed via ``isaacteleop_wheel`` + because it is not authored under ``src/python/``) and splices its ``__all__``. The legacy `isaacteleop.deviceio` shim is a **frozen** compat + surface — do not extend it with new generated trackers. + +**Still hand-written:** the `.fbs` schema and its `schema/python/*_bindings.h` pybind file (codegen +starts at the tracker layer, not the schema layer); `head`, `hand`, `controller`, `full_body`, +`message_channel`, `HapticCommandReaderTracker` (multi-sample bucketing by +`HapticCommand.endpoint` on one push-tensor collection); +and `FrameMetadataTrackerOak`, whose N-collection-per-stream shape does not fit the single-collection +template. + ## No OpenXR dependency - **`deviceio_trackers`** must **not** link **`OpenXR::headers`**, **`oxr::oxr_utils`**, or vendor extension targets, and must **not** `#include` OpenXR headers. Public API stays schema + **`deviceio_base`** only. - This includes **`tracker_bindings.cpp`**: do not add `#include ` or any `XR_NV_*` extension headers here, even when the bound tracker wraps an OpenXR concept. The UUID is `std::array` at the `deviceio_trackers` boundary—no OpenXR types leak through. +- The generated facades hold the same line: they take a `collection_id` string and dispatch through + `deviceio_base`, so OpenXR stays on the `live_trackers` side of the boundary. ## Related docs +- Manifest and generator rules: [`../codegen/AGENTS.md`](../codegen/AGENTS.md) - Base interface: [`../deviceio_base/AGENTS.md`](../deviceio_base/AGENTS.md) +- Live impls and factory: [`../live_trackers/AGENTS.md`](../live_trackers/AGENTS.md) +- Replay impls and factory: [`../replay_trackers/AGENTS.md`](../replay_trackers/AGENTS.md) diff --git a/src/core/deviceio_trackers/cpp/CMakeLists.txt b/src/core/deviceio_trackers/cpp/CMakeLists.txt index 1b432d7e9..a4d51a4dc 100644 --- a/src/core/deviceio_trackers/cpp/CMakeLists.txt +++ b/src/core/deviceio_trackers/cpp/CMakeLists.txt @@ -9,31 +9,27 @@ add_library(deviceio_trackers STATIC head_tracker.cpp controller_tracker.cpp message_channel_tracker.cpp - generic_3axis_pedal_tracker.cpp - oglo_tactile_tracker.cpp tensor_push_tracker.cpp - haptic_command_reader_tracker.cpp - joint_state_tracker.cpp - se3_tracker.cpp frame_metadata_tracker_oak.cpp full_body_tracker.cpp + haptic_command_reader_tracker.cpp + ${GENERATED_TRACKER_FACADE_SOURCES} 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.hpp - inc/deviceio_trackers/generic_3axis_pedal_tracker.hpp - inc/deviceio_trackers/oglo_tactile_tracker.hpp inc/deviceio_trackers/tensor_push_tracker.hpp - inc/deviceio_trackers/haptic_command_reader_tracker.hpp - inc/deviceio_trackers/joint_state_tracker.hpp - inc/deviceio_trackers/se3_tracker.hpp inc/deviceio_trackers/frame_metadata_tracker_oak.hpp + inc/deviceio_trackers/haptic_command_reader_tracker.hpp ) target_include_directories(deviceio_trackers - INTERFACE + PUBLIC $ + $ + $ + $ ) target_link_libraries(deviceio_trackers diff --git a/src/core/deviceio_trackers/cpp/generic_3axis_pedal_tracker.cpp b/src/core/deviceio_trackers/cpp/generic_3axis_pedal_tracker.cpp deleted file mode 100644 index 6fc13dc5b..000000000 --- a/src/core/deviceio_trackers/cpp/generic_3axis_pedal_tracker.cpp +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -#include "inc/deviceio_trackers/generic_3axis_pedal_tracker.hpp" - -namespace core -{ - -// ============================================================================ -// Generic3AxisPedalTracker -// ============================================================================ - -Generic3AxisPedalTracker::Generic3AxisPedalTracker(const std::string& collection_id, size_t max_flatbuffer_size) - : collection_id_(collection_id), max_flatbuffer_size_(max_flatbuffer_size) -{ -} - -const Generic3AxisPedalOutputTrackedT& Generic3AxisPedalTracker::get_data(const ITrackerSession& session) const -{ - return static_cast(session.get_tracker_impl(*this)).get_data(); -} - -} // namespace core diff --git a/src/core/deviceio_trackers/cpp/inc/deviceio_trackers/generic_3axis_pedal_tracker.hpp b/src/core/deviceio_trackers/cpp/inc/deviceio_trackers/generic_3axis_pedal_tracker.hpp deleted file mode 100644 index 30462457a..000000000 --- a/src/core/deviceio_trackers/cpp/inc/deviceio_trackers/generic_3axis_pedal_tracker.hpp +++ /dev/null @@ -1,87 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -#pragma once - -#include -#include - -#include -#include - -namespace core -{ - -/*! - * @brief Facade for three-axis pedal state exposed as ``Generic3AxisPedalOutputTrackedT``. - * - * Semantic contract: ``left_pedal``, ``right_pedal``, and ``rudder`` are scalar floats matching the - * ``Generic3AxisPedalOutput`` schema (axis semantics are left/right/rudder as named). Units, range, - * and calibration (e.g. normalized vs raw device values) are defined by the data producer unless - * documented elsewhere. After each ``ITrackerSession::update()`` that includes this tracker, ``get_data(session)`` - * reflects the implementation’s tracked snapshot. The live backend (``LiveGeneric3AxisPedalTrackerImpl::update``) - * may retain the **last-known** sample when a tick has **no new** samples (e.g. ``m_pending_records`` empty after - * ``read_all_samples``) while the tensor collection remains available — in that case ``data`` stays non-null but - * may be **stale** relative to the latest device state. Separately, **absent** data (``data`` null) means no sample - * has been unpacked yet or the collection/source is unavailable and the implementation has cleared state. - * Implementations may obtain these values through different - * backends; transport-specific setup (buffers, extensions, discovery) is documented with the live - * ``ITrackerImpl`` and session factory. - * - * Usage: - * @code - * auto tracker = std::make_shared("my_pedal_collection"); - * // ... register the tracker with a session, then each tick: ... - * session->update(); - * const auto& data = tracker->get_data(*session); - * @endcode - */ -class Generic3AxisPedalTracker : public ITracker -{ -public: - //! Default maximum FlatBuffer size for Generic3AxisPedalOutput messages. - static constexpr size_t DEFAULT_MAX_FLATBUFFER_SIZE = 256; - - /*! - * @brief Constructs a Generic3AxisPedalTracker. - * @param collection_id Logical stream identifier; must match the data source for the chosen backend - * (see live implementation documentation). - * @param max_flatbuffer_size Upper bound for serialized ``Generic3AxisPedalOutput`` / record payloads - * (default: 256 bytes); must be sufficient for the schema and backend. - */ - explicit Generic3AxisPedalTracker(const std::string& collection_id, - size_t max_flatbuffer_size = DEFAULT_MAX_FLATBUFFER_SIZE); - - std::string_view get_name() const override - { - return TRACKER_NAME; - } - /*! - * @brief Pedal snapshot from the session’s implementation. - * - * ``tracked.data`` is null when there is no valid last-known sample (source never provided data or - * implementation cleared state when the collection is gone). When non-null, values may still be **unchanged** - * from the previous ``update()`` if that tick produced no new samples (see ``LiveGeneric3AxisPedalTrackerImpl`` - * and ``m_pending_records``). When ``tracked.data`` is non-null, nested fields in - * ``Generic3AxisPedalOutputT`` are safe to read. - */ - const Generic3AxisPedalOutputTrackedT& get_data(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 = "Generic3AxisPedalTracker"; - - std::string collection_id_; - size_t max_flatbuffer_size_; -}; - -} // namespace core diff --git a/src/core/deviceio_trackers/cpp/inc/deviceio_trackers/joint_state_tracker.hpp b/src/core/deviceio_trackers/cpp/inc/deviceio_trackers/joint_state_tracker.hpp deleted file mode 100644 index 97a53b406..000000000 --- a/src/core/deviceio_trackers/cpp/inc/deviceio_trackers/joint_state_tracker.hpp +++ /dev/null @@ -1,84 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -#pragma once - -#include -#include - -#include -#include - -namespace core -{ - -/*! - * @brief Facade for a generic joint-space device exposed as ``JointStateOutputTrackedT``. - * - * Generic across joint-space input devices (leader arms, exoskeletons, gloves, ...): the payload - * is a list of named joints (``JointStateOutput.joints``, keyed by ``JointState.name``) plus an - * optional end-effector pose. The semantics of each joint (units, calibration) are defined by the - * data producer (the device plugin). A distinct ``collection_id`` per device allows several - * joint-space devices to stream simultaneously. - * - * After each ``ITrackerSession::update()`` that includes this tracker, ``get_data(session)`` - * reflects the implementation's tracked snapshot. As with other ``SchemaTracker``-backed trackers, - * the live backend may retain the last-known sample when a tick has no new samples while the - * collection remains available (``data`` stays non-null but may be stale); ``data`` is null only - * when no sample has arrived yet or the collection is unavailable. - * - * Usage: - * @code - * auto tracker = std::make_shared("so101_leader"); - * // ... register the tracker with a session, then each tick: ... - * session->update(); - * const auto& data = tracker->get_data(*session); - * @endcode - */ -class JointStateTracker : public ITracker -{ -public: - //! Default maximum FlatBuffer size for JointStateOutput messages. - //! Large enough for a few dozen named joints with optional velocity/effort. Pusher and tracker - //! must agree on this value (it sizes the fixed tensor buffer). - static constexpr size_t DEFAULT_MAX_FLATBUFFER_SIZE = 4096; - - /*! - * @brief Constructs a JointStateTracker. - * @param collection_id Logical stream identifier; must match the device plugin / pusher. - * @param max_flatbuffer_size Upper bound for serialized ``JointStateOutput`` / record payloads. - */ - explicit JointStateTracker(const std::string& collection_id, - size_t max_flatbuffer_size = DEFAULT_MAX_FLATBUFFER_SIZE); - - std::string_view get_name() const override - { - return TRACKER_NAME; - } - - /*! - * @brief Joint-state snapshot from the session's implementation. - * - * ``tracked.data`` is null when no valid sample exists. When non-null, the nested - * ``JointStateOutputT`` (joints, device_id, optional ee_pose) is safe to read. - */ - const JointStateOutputTrackedT& get_data(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 = "JointStateTracker"; - - std::string collection_id_; - size_t max_flatbuffer_size_; -}; - -} // namespace core diff --git a/src/core/deviceio_trackers/cpp/inc/deviceio_trackers/oglo_tactile_tracker.hpp b/src/core/deviceio_trackers/cpp/inc/deviceio_trackers/oglo_tactile_tracker.hpp deleted file mode 100644 index f1c9a9944..000000000 --- a/src/core/deviceio_trackers/cpp/inc/deviceio_trackers/oglo_tactile_tracker.hpp +++ /dev/null @@ -1,79 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -#pragma once - -#include -#include - -#include -#include - -namespace core -{ - -/*! - * @brief Facade for one OGLO tactile glove exposed as ``OgloGloveSampleTrackedT``. - * - * Reads a tensor collection pushed by the ``oglo_tactile`` plugin - * (``--collection-prefix``). One tracker per hand: construct with the matching - * ``collection_id`` (e.g. ``"oglo/left"`` / ``"oglo/right"``). After each - * ``ITrackerSession::update()`` that includes this tracker, ``get_data(session)`` - * reflects the latest decoded sample; ``data`` is null until the first sample - * arrives or when the collection is unavailable. - * - * Usage: - * @code - * auto glove = std::make_shared("oglo/right"); - * // ... register with a session, then each tick: ... - * session->update(); - * const auto& tracked = glove->get_data(*session); - * if (tracked.data) { auto& taxels = tracked.data->taxels; ... } - * @endcode - */ -class OgloTactileTracker : public ITracker -{ -public: - //! Default maximum FlatBuffer size for OgloGloveSample messages - //! (80 taxels x 2B + 6 IMU x 2B + table overhead). - static constexpr size_t DEFAULT_MAX_FLATBUFFER_SIZE = 512; - - /*! - * @brief Constructs an OgloTactileTracker. - * @param collection_id Tensor collection identifier; must match the - * ``oglo_tactile`` plugin's ``--collection-prefix`` + "/" + side. - * @param max_flatbuffer_size Upper bound for serialized payloads (default 512). - */ - explicit OgloTactileTracker(const std::string& collection_id, - size_t max_flatbuffer_size = DEFAULT_MAX_FLATBUFFER_SIZE); - - std::string_view get_name() const override - { - return TRACKER_NAME; - } - - /*! - * @brief Glove snapshot from the session's implementation. - * @c tracked.data is null when no valid sample is available; when non-null, - * @c data->taxels (80 values) and the IMU fields are safe to read. - */ - const OgloGloveSampleTrackedT& get_data(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 = "OgloTactileTracker"; - - std::string collection_id_; - size_t max_flatbuffer_size_; -}; - -} // namespace core diff --git a/src/core/deviceio_trackers/cpp/inc/deviceio_trackers/se3_tracker.hpp b/src/core/deviceio_trackers/cpp/inc/deviceio_trackers/se3_tracker.hpp deleted file mode 100644 index f2b701aa5..000000000 --- a/src/core/deviceio_trackers/cpp/inc/deviceio_trackers/se3_tracker.hpp +++ /dev/null @@ -1,95 +0,0 @@ -// 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 -{ - -/*! - * @brief Facade for a generic SE3 (6-DoF pose) tracker device exposed as ``Se3TrackerPoseTrackedT``. - * - * Generic across rigid-body pose sources (tracker pucks, mocap rigid bodies, logical trackers - * derived from other devices, ...): the payload is a single pose plus a validity flag. The - * reference frame is defined by the data producer (see ``se3_tracker.fbs`` for the normative - * conventions). A distinct ``collection_id`` per device allows several SE3 trackers to stream - * simultaneously. - * - * After each ``ITrackerSession::update()`` that includes this tracker, ``get_data(session)`` - * reflects the implementation's tracked snapshot. As with other ``SchemaTracker``-backed trackers, - * the live backend may retain the last-known sample when a tick has no new samples while the - * collection remains available (``data`` stays non-null but may be stale); ``data`` is null only - * when no sample has arrived yet or the collection is unavailable. Independently, - * ``data->is_valid == false`` means the producer is streaming but tracking is lost — the pose - * contents are then unspecified. - * - * Note: ``collection_id`` (stream instance), ``TENSOR_IDENTIFIER`` (tensor name within the - * collection), and the MCAP channel names are independent identifiers that happen to share the - * default spelling ``se3_tracker``. - * - * Usage: - * @code - * auto tracker = std::make_shared("se3_tracker"); - * // ... register the tracker with a session, then each tick: ... - * session->update(); - * const auto& data = tracker->get_data(*session); - * @endcode - */ -class Se3Tracker : public ITracker -{ -public: - //! Default maximum FlatBuffer size for Se3TrackerPose messages. Pusher and tracker must agree - //! on this value (it sizes the fixed tensor buffer). - static constexpr size_t DEFAULT_MAX_FLATBUFFER_SIZE = 256; - - //! Tensor name within the collection. Single source of truth for the pusher/reader wire - //! rendezvous: both LiveSe3TrackerImpl and producer plugins reference this symbol (a mismatch - //! is silent no-data). - static constexpr std::string_view TENSOR_IDENTIFIER = "se3_tracker"; - - /*! - * @brief Constructs an Se3Tracker. - * @param collection_id Logical stream identifier; must match the device plugin / pusher. - * @param max_flatbuffer_size Upper bound for serialized ``Se3TrackerPose`` / record payloads. - */ - explicit Se3Tracker(const std::string& collection_id, size_t max_flatbuffer_size = DEFAULT_MAX_FLATBUFFER_SIZE); - - std::string_view get_name() const override - { - return TRACKER_NAME; - } - - /*! - * @brief SE3 tracker snapshot from the session's implementation. - * - * ``tracked.data`` is null when no sample has arrived yet or the collection is unavailable. - * When non-null, gate on ``data->is_valid`` before consuming ``data->pose`` — the pose is - * unspecified while tracking is lost. - */ - const Se3TrackerPoseTrackedT& get_data(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 = "Se3Tracker"; - - std::string collection_id_; - size_t max_flatbuffer_size_; -}; - -} // namespace core diff --git a/src/core/deviceio_trackers/cpp/joint_state_tracker.cpp b/src/core/deviceio_trackers/cpp/joint_state_tracker.cpp deleted file mode 100644 index 5766ffef5..000000000 --- a/src/core/deviceio_trackers/cpp/joint_state_tracker.cpp +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -#include "inc/deviceio_trackers/joint_state_tracker.hpp" - -namespace core -{ - -// ============================================================================ -// JointStateTracker -// ============================================================================ - -JointStateTracker::JointStateTracker(const std::string& collection_id, size_t max_flatbuffer_size) - : collection_id_(collection_id), max_flatbuffer_size_(max_flatbuffer_size) -{ -} - -const JointStateOutputTrackedT& JointStateTracker::get_data(const ITrackerSession& session) const -{ - return static_cast(session.get_tracker_impl(*this)).get_data(); -} - -} // namespace core diff --git a/src/core/deviceio_trackers/cpp/oglo_tactile_tracker.cpp b/src/core/deviceio_trackers/cpp/oglo_tactile_tracker.cpp deleted file mode 100644 index 851911d15..000000000 --- a/src/core/deviceio_trackers/cpp/oglo_tactile_tracker.cpp +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -#include "inc/deviceio_trackers/oglo_tactile_tracker.hpp" - -namespace core -{ - -// ============================================================================ -// OgloTactileTracker -// ============================================================================ - -OgloTactileTracker::OgloTactileTracker(const std::string& collection_id, size_t max_flatbuffer_size) - : collection_id_(collection_id), max_flatbuffer_size_(max_flatbuffer_size) -{ -} - -const OgloGloveSampleTrackedT& OgloTactileTracker::get_data(const ITrackerSession& session) const -{ - return static_cast(session.get_tracker_impl(*this)).get_data(); -} - -} // namespace core diff --git a/src/core/deviceio_trackers/cpp/se3_tracker.cpp b/src/core/deviceio_trackers/cpp/se3_tracker.cpp deleted file mode 100644 index 1f09ebeab..000000000 --- a/src/core/deviceio_trackers/cpp/se3_tracker.cpp +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -#include "inc/deviceio_trackers/se3_tracker.hpp" - -namespace core -{ - -// ============================================================================ -// Se3Tracker -// ============================================================================ - -Se3Tracker::Se3Tracker(const std::string& collection_id, size_t max_flatbuffer_size) - : collection_id_(collection_id), max_flatbuffer_size_(max_flatbuffer_size) -{ -} - -const Se3TrackerPoseTrackedT& Se3Tracker::get_data(const ITrackerSession& session) const -{ - return static_cast(session.get_tracker_impl(*this)).get_data(); -} - -} // namespace core diff --git a/src/core/deviceio_trackers/defaults.toml b/src/core/deviceio_trackers/defaults.toml new file mode 100644 index 000000000..92151fb1d --- /dev/null +++ b/src/core/deviceio_trackers/defaults.toml @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Default tracker manifest values. Placeholders use %key% or %key_CamelCase% / %key_UPPER%. +# See src/core/codegen/manifest.py for resolution rules. + +[defaults] +direction = "in" +shape = "single_collection" +schema = "%name%" +class = "%name_CamelCase%Tracker" +tensor_identifier = "%name%" +localized_name = "%class%" +channel = "%name%" +schema_name = "core.%table%Record" +traits = "%channel_CamelCase%RecordingTraits" +python_accessor = "get_data" +max_flatbuffer_size = 512 +record = true +facade_tensor_constant = false + +[defaults.in] +mcap_channels = ["%channel%", "%channel%_tracked"] +replay_channels = ["%channel%_tracked"] + +[defaults.out] +record = false +class = "%table%PushTracker" +python_accessor = "push" diff --git a/src/core/deviceio_trackers/python/CMakeLists.txt b/src/core/deviceio_trackers/python/CMakeLists.txt index 34dc17908..475e26a48 100644 --- a/src/core/deviceio_trackers/python/CMakeLists.txt +++ b/src/core/deviceio_trackers/python/CMakeLists.txt @@ -10,6 +10,11 @@ target_link_libraries(deviceio_trackers_py deviceio::deviceio_trackers ) +target_include_directories(deviceio_trackers_py + PRIVATE + ${GENERATED_TRACKER_INC_DIR} +) + set_target_properties(deviceio_trackers_py PROPERTIES OUTPUT_NAME "_deviceio_trackers" LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/python_package/$/isaacteleop/deviceio_trackers" diff --git a/src/core/deviceio_trackers/python/tracker_bindings.cpp b/src/core/deviceio_trackers/python/tracker_bindings.cpp index a36b99213..7d6e1824d 100644 --- a/src/core/deviceio_trackers/python/tracker_bindings.cpp +++ b/src/core/deviceio_trackers/python/tracker_bindings.cpp @@ -1,16 +1,15 @@ // SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 +#include "generated_tracker_binding_includes.inc" + #include #include #include -#include #include +#include #include -#include #include -#include -#include #include #include #include @@ -21,6 +20,7 @@ #include #include #include +#include namespace py = pybind11; @@ -182,30 +182,26 @@ PYBIND11_MODULE(_deviceio_trackers, m) .def_property_readonly("stream_count", &core::FrameMetadataTrackerOak::get_stream_count, "Number of streams this tracker is configured for"); - py::class_>( - m, "Generic3AxisPedalTracker") - .def(py::init(), py::arg("collection_id"), - py::arg("max_flatbuffer_size") = core::Generic3AxisPedalTracker::DEFAULT_MAX_FLATBUFFER_SIZE, - "Construct a Generic3AxisPedalTracker for the given tensor collection ID") + py::class_>( + m, "HapticCommandReaderTracker") + .def(py::init(), py::arg("collection_id"), + py::arg("max_payload_size") = core::HapticCommandReaderTracker::DEFAULT_MAX_PAYLOAD_SIZE) .def( - "get_pedal_data", - [](const core::Generic3AxisPedalTracker& self, const core::ITrackerSession& session) + "get_data", + [](const core::HapticCommandReaderTracker& self, const core::ITrackerSession& session) { return share_tracked(self.get_data(session)); }, py::arg("session"), - "Get the current foot pedal tracked state (data is None when no data available)" TRACKED_LIFETIME_DOC); - - py::class_>( - m, "OgloTactileTracker") - .def(py::init(), py::arg("collection_id"), - py::arg("max_flatbuffer_size") = core::OgloTactileTracker::DEFAULT_MAX_FLATBUFFER_SIZE, - "Construct an OgloTactileTracker for the given tensor collection ID " - "(e.g. 'oglo/left' / 'oglo/right', matching the oglo_tactile plugin's --collection-prefix)") + "Get the latest haptic command tracked state (data is None when no data available)" TRACKED_LIFETIME_DOC) .def( - "get_glove_data", - [](const core::OgloTactileTracker& self, const core::ITrackerSession& session) - { return share_tracked(self.get_data(session)); }, - py::arg("session"), - "Get the current tactile glove tracked state (data is None when no data available)" TRACKED_LIFETIME_DOC); + "get_data", + [](const core::HapticCommandReaderTracker& self, const core::ITrackerSession& session, + std::string_view endpoint) { return share_tracked(self.get_data(session, endpoint)); }, + py::arg("session"), py::arg("endpoint"), + "Get the latest haptic command for one endpoint" TRACKED_LIFETIME_DOC); + + // py::class_ blocks for every manifest tracker; the accessor name comes from the + // manifest's python_accessor key. +#include "generated_tracker_bindings.inc" py::class_> tensor_push_tracker( m, "TensorPushTracker"); @@ -226,30 +222,6 @@ PYBIND11_MODULE(_deviceio_trackers, m) }, py::arg("session"), py::arg("payload"), "Push one serialized payload (bytes, length <= max_payload_size) to the paired consumer."); - py::class_>(m, "JointStateTracker") - .def(py::init(), py::arg("collection_id"), - py::arg("max_flatbuffer_size") = core::JointStateTracker::DEFAULT_MAX_FLATBUFFER_SIZE, - "Construct a JointStateTracker for the given tensor collection ID (one generic " - "joint-space device: leader arm, exoskeleton, ...)") - .def( - "get_data", - [](const core::JointStateTracker& self, const core::ITrackerSession& session) - { return share_tracked(self.get_data(session)); }, - py::arg("session"), - "Get the current joint-state tracked state (data is None when no data available)" TRACKED_LIFETIME_DOC); - - py::class_>(m, "Se3Tracker") - .def(py::init(), py::arg("collection_id"), - py::arg("max_flatbuffer_size") = core::Se3Tracker::DEFAULT_MAX_FLATBUFFER_SIZE, - "Construct an Se3Tracker for the given tensor collection ID (one generic SE3 " - "6-DoF pose source: tracker puck, mocap rigid body, logical tracker, ...)") - .def( - "get_data", - [](const core::Se3Tracker& self, const core::ITrackerSession& session) - { return share_tracked(self.get_data(session)); }, - py::arg("session"), - "Get the current SE3 tracked state (data is None when no data available; gate on " - "data.is_valid before consuming the pose)" TRACKED_LIFETIME_DOC); py::class_>(m, "FullBodyTracker") .def(py::init<>(), diff --git a/src/core/deviceio_trackers/trackers.toml b/src/core/deviceio_trackers/trackers.toml new file mode 100644 index 000000000..931085139 --- /dev/null +++ b/src/core/deviceio_trackers/trackers.toml @@ -0,0 +1,37 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +[[tracker]] +name = "joint_state" +table = "JointStateOutput" +max_flatbuffer_size = 4096 + +[[tracker]] +name = "se3_tracker" +table = "Se3TrackerPose" +class = "Se3Tracker" +max_flatbuffer_size = 256 +facade_tensor_constant = true + +[[tracker]] +name = "oglo_tactile" +table = "OgloGloveSample" +channel = "oglo" +python_accessor = "get_glove_data" + +[[tracker]] +name = "generic_3axis_pedal" +table = "Generic3AxisPedalOutput" +schema = "pedals" +channel = "pedals" +traits = "PedalRecordingTraits" +max_flatbuffer_size = 256 +python_accessor = "get_pedal_data" + +[[tracker]] +name = "haptic_command" +direction = "out" +table = "HapticCommand" +schema = "haptic_command" +tensor_identifier = "haptic_command" +max_flatbuffer_size = 256 diff --git a/src/core/live_trackers/AGENTS.md b/src/core/live_trackers/AGENTS.md index 8d882310d..8c28bb24d 100644 --- a/src/core/live_trackers/AGENTS.md +++ b/src/core/live_trackers/AGENTS.md @@ -28,20 +28,40 @@ SPDX-License-Identifier: Apache-2.0 - **`live_trackers`** should **`PUBLIC` link `oxr::oxr_utils`** (OpenXR headers come through that INTERFACE target) because headers/sources use OpenXR / oxr types. +## Passthrough impls are generated + +Live impls for trackers declared in +[`../deviceio_trackers/trackers.toml`](../deviceio_trackers/trackers.toml) are emitted into +`${CMAKE_BINARY_DIR}/generated/trackers/live_trackers/`; only hand-written trackers have `.cpp` +files in this directory. `live_deviceio_factory.{hpp,cpp}` stays hand-written but `#include`s +generated `.inc` fragments for the manifest trackers' forward decls, try-create thunks, dispatch +rows, and factory methods — **do not** add rows for a manifest tracker by hand. + +Vendor routing is the reason the generated dispatch rows sit as one block at the end of +`k_tracker_dispatch`: manifest trackers are single-vendor, so their row order does not matter, +while multi-vendor hand-written types must keep their default vendor first. + ## New tracker MCAP checklist -When adding MCAP support to a new tracker impl, all of the following are required together—missing any one causes a build failure or wrong timestamps: +Applies to **hand-written** live tracker impls (`head`, `hand`, `controller`, `full_body`, +`message_channel`, `FrameMetadataTrackerOak`, …). For manifest trackers the impl, its MCAP +channels, and its recording traits are all generated — skip this checklist entirely. + +When adding MCAP support to a new **hand-written** tracker impl, all of the following are required together—missing any one causes a build failure or wrong timestamps: 1. Add `XrTimeConverter time_converter_` and `int64_t last_update_time_ = 0` members to the impl header. 2. Initialize `time_converter_(handles)` in the constructor initializer list. 3. Declare `update(int64_t monotonic_time_ns) override` (not `XrTime`)—they are the same C++ type (`int64_t`) but semantically different; the base interface uses monotonic ns. 4. At the top of `update()`: store `last_update_time_ = monotonic_time_ns` and compute `const XrTime xr_time = time_converter_.convert_monotonic_ns_to_xrtime(monotonic_time_ns)`. 5. Use `DeviceDataTimestamp(last_update_time_, last_update_time_, xr_time)` — not `(time, time, time)`. -6. Add `MessageChannelRecordingTraits` (or equivalent) to `recording_traits.hpp`. +6. Add `MessageChannelRecordingTraits` (or equivalent) to `recording_traits.hpp` **above** its + `generated_recording_traits.inc` include — that fragment is the manifest trackers' half. 7. **Always build** (`cmake --build -- -j$(nproc)`) before treating work as done. Pre-commit alone does not catch compile errors or clang-format violations enforced at build time. 8. Read `AGENTS.md` before starting. Not after CI breaks. ## Related docs +- Manifest and generator rules: [`../codegen/AGENTS.md`](../codegen/AGENTS.md) - Session update loop: [`../deviceio_session/AGENTS.md`](../deviceio_session/AGENTS.md) - No OpenXR in base API: [`../deviceio_base/AGENTS.md`](../deviceio_base/AGENTS.md) +- Replay counterpart: [`../replay_trackers/AGENTS.md`](../replay_trackers/AGENTS.md) diff --git a/src/core/live_trackers/cpp/CMakeLists.txt b/src/core/live_trackers/cpp/CMakeLists.txt index 3679c45a5..01c6e3b03 100644 --- a/src/core/live_trackers/cpp/CMakeLists.txt +++ b/src/core/live_trackers/cpp/CMakeLists.txt @@ -12,13 +12,10 @@ add_library(live_trackers STATIC live_message_channel_tracker_impl.cpp live_full_body_tracker_pico_impl.cpp live_full_body_tracker_noitom_impl.cpp - live_generic_3axis_pedal_tracker_impl.cpp - live_oglo_tactile_tracker_impl.cpp live_tensor_push_tracker_impl.cpp - live_haptic_command_reader_tracker_impl.cpp - live_joint_state_tracker_impl.cpp - live_se3_tracker_impl.cpp live_frame_metadata_tracker_oak_impl.cpp + live_haptic_command_reader_tracker_impl.cpp + ${GENERATED_TRACKER_LIVE_SOURCES} inc/live_trackers/schema_tracker_base.hpp inc/live_trackers/schema_tracker.hpp inc/live_trackers/live_deviceio_factory.hpp @@ -28,18 +25,17 @@ add_library(live_trackers STATIC live_message_channel_tracker_impl.hpp live_full_body_tracker_pico_impl.hpp live_full_body_tracker_noitom_impl.hpp - live_generic_3axis_pedal_tracker_impl.hpp - live_oglo_tactile_tracker_impl.hpp live_tensor_push_tracker_impl.hpp - live_haptic_command_reader_tracker_impl.hpp - live_joint_state_tracker_impl.hpp - live_se3_tracker_impl.hpp live_frame_metadata_tracker_oak_impl.hpp + live_haptic_command_reader_tracker_impl.hpp ) target_include_directories(live_trackers - INTERFACE + PUBLIC $ + $ + $ + $ ) target_link_libraries(live_trackers 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 a6a8ffab2..0262982ef 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 @@ -31,24 +31,20 @@ class MessageChannelTracker; class IMessageChannelTrackerImpl; class FullBodyTracker; class IFullBodyTrackerImpl; -class Generic3AxisPedalTracker; -class IGeneric3AxisPedalTrackerImpl; -class OgloTactileTracker; -class IOgloTactileTrackerImpl; class TensorPushTracker; class ITensorPushTrackerImpl; -class HapticCommandReaderTracker; -class IHapticCommandReaderTrackerImpl; -class JointStateTracker; -class IJointStateTrackerImpl; -class Se3Tracker; -class ISe3TrackerImpl; class HandTracker; class IHandTrackerImpl; class HeadTracker; class IHeadTrackerImpl; +class HapticCommandReaderTracker; +class IHapticCommandReaderTrackerImpl; struct OpenXRSessionHandles; +// Forward decls for trackers declared in deviceio_trackers/trackers.toml. Generated at +// configure time; do not add a row here for a manifest tracker. +#include "generated_tracker_forward_decls.inc" + /** * @brief Factory for live OpenXR tracker implementations. * @@ -59,26 +55,12 @@ struct OpenXRSessionHandles; class LiveDeviceIOFactory { public: - /** - * @brief Aggregate OpenXR extensions required by the given trackers for a live session. - * - * Each tracker resolves its required extensions through the dispatch table using the vendor - * id selected in @p tracker_vendors (or its default vendor when unlisted). - * - * @pre @p tracker_vendors is a validated vendor config (see validate_vendor_selections()). - * Passing an invalid config is undefined behavior; DeviceIOSession validates before - * calling this. - */ static std::vector get_required_extensions( const std::vector>& trackers, const std::vector>& tracker_vendors = {}); - /** Create tracker impl from a tracker instance using the same dispatch as extension discovery. */ std::unique_ptr create_tracker_impl(const ITracker& tracker); - // @pre @p tracker_vendors is a validated vendor config (see validate_vendor_selections()). - // The factory assumes validity; passing an invalid config is undefined behavior. - // DeviceIOSession validates before constructing the factory. LiveDeviceIOFactory(const OpenXRSessionHandles& handles, mcap::McapWriter* writer, const std::vector>& tracker_names, @@ -90,24 +72,19 @@ class LiveDeviceIOFactory std::unique_ptr create_message_channel_tracker_impl(const MessageChannelTracker* tracker); std::unique_ptr create_full_body_tracker_pico_impl(const FullBodyTracker* tracker); std::unique_ptr create_full_body_tracker_noitom_impl(const FullBodyTracker* tracker); - std::unique_ptr create_generic_3axis_pedal_tracker_impl( - const Generic3AxisPedalTracker* tracker); - std::unique_ptr create_oglo_tactile_tracker_impl(const OgloTactileTracker* tracker); std::unique_ptr create_tensor_push_tracker_impl(const TensorPushTracker* tracker); - std::unique_ptr create_haptic_command_reader_tracker_impl( - const HapticCommandReaderTracker* tracker); - std::unique_ptr create_joint_state_tracker_impl(const JointStateTracker* tracker); - std::unique_ptr create_se3_tracker_impl(const Se3Tracker* tracker); std::unique_ptr create_frame_metadata_tracker_oak_impl( const FrameMetadataTrackerOak* tracker); + std::unique_ptr create_haptic_command_reader_tracker_impl( + const HapticCommandReaderTracker* tracker); + // create__tracker_impl for every manifest tracker. +#include "generated_live_factory_declarations.inc" private: - // Per-tracker data resolved from the session config: MCAP channel base name (recording) and - // vendor selection. A tracker appears only when it has one or the other. struct TrackerData { - std::optional name; // MCAP channel base name; absent -> not recorded. - std::optional vendor; // vendor selection; absent -> default vendor id. + std::optional name; + std::optional vendor; }; bool should_record(const ITracker* tracker) const; diff --git a/src/core/live_trackers/cpp/live_deviceio_factory.cpp b/src/core/live_trackers/cpp/live_deviceio_factory.cpp index 940e7b879..16665e5e6 100644 --- a/src/core/live_trackers/cpp/live_deviceio_factory.cpp +++ b/src/core/live_trackers/cpp/live_deviceio_factory.cpp @@ -3,31 +3,24 @@ #include "inc/live_trackers/live_deviceio_factory.hpp" +#include "generated_live_includes.inc" #include "live_controller_tracker_impl.hpp" #include "live_frame_metadata_tracker_oak_impl.hpp" #include "live_full_body_tracker_noitom_impl.hpp" #include "live_full_body_tracker_pico_impl.hpp" -#include "live_generic_3axis_pedal_tracker_impl.hpp" #include "live_hand_tracker_impl.hpp" #include "live_haptic_command_reader_tracker_impl.hpp" #include "live_head_tracker_impl.hpp" -#include "live_joint_state_tracker_impl.hpp" #include "live_message_channel_tracker_impl.hpp" -#include "live_oglo_tactile_tracker_impl.hpp" -#include "live_se3_tracker_impl.hpp" #include "live_tensor_push_tracker_impl.hpp" #include #include #include -#include #include #include #include -#include #include -#include -#include #include #include @@ -101,48 +94,26 @@ std::unique_ptr try_create_full_body_noitom_impl(LiveDeviceIOFacto return typed ? factory.create_full_body_tracker_noitom_impl(typed) : nullptr; } -std::unique_ptr try_create_generic_pedal_impl(LiveDeviceIOFactory& factory, const ITracker& tracker) -{ - auto* typed = dynamic_cast(&tracker); - return typed ? factory.create_generic_3axis_pedal_tracker_impl(typed) : nullptr; -} - std::unique_ptr try_create_tensor_push_impl(LiveDeviceIOFactory& factory, const ITracker& tracker) { auto* typed = dynamic_cast(&tracker); return typed ? factory.create_tensor_push_tracker_impl(typed) : nullptr; } -std::unique_ptr try_create_haptic_command_reader_impl(LiveDeviceIOFactory& factory, const ITracker& tracker) -{ - auto* typed = dynamic_cast(&tracker); - return typed ? factory.create_haptic_command_reader_tracker_impl(typed) : nullptr; -} - -std::unique_ptr try_create_joint_state_impl(LiveDeviceIOFactory& factory, const ITracker& tracker) -{ - auto* typed = dynamic_cast(&tracker); - return typed ? factory.create_joint_state_tracker_impl(typed) : nullptr; -} - -std::unique_ptr try_create_se3_tracker_impl(LiveDeviceIOFactory& factory, const ITracker& tracker) -{ - auto* typed = dynamic_cast(&tracker); - return typed ? factory.create_se3_tracker_impl(typed) : nullptr; -} - std::unique_ptr try_create_oak_impl(LiveDeviceIOFactory& factory, const ITracker& tracker) { auto* typed = dynamic_cast(&tracker); return typed ? factory.create_frame_metadata_tracker_oak_impl(typed) : nullptr; } -std::unique_ptr try_create_oglo_impl(LiveDeviceIOFactory& factory, const ITracker& tracker) +std::unique_ptr try_create_haptic_command_reader_impl(LiveDeviceIOFactory& factory, const ITracker& tracker) { - auto* typed = dynamic_cast(&tracker); - return typed ? factory.create_oglo_tactile_tracker_impl(typed) : nullptr; + auto* typed = dynamic_cast(&tracker); + return typed ? factory.create_haptic_command_reader_tracker_impl(typed) : nullptr; } +#include "generated_live_try_create.inc" + using CollectExtensionsFn = bool (*)(const ITracker&, std::set&); using TypeMatchesFn = bool (*)(const ITracker&); using TryCreateFn = std::unique_ptr (*)(LiveDeviceIOFactory&, const ITracker&); @@ -181,14 +152,12 @@ inline const TrackerDispatchEntry k_tracker_dispatch[] = { make_dispatch_entry(&try_create_full_body_pico_impl, "body.pico-xr"), make_dispatch_entry( &try_create_full_body_noitom_impl, LiveFullBodyTrackerNoitomImpl::VENDOR_ID), - make_dispatch_entry(&try_create_generic_pedal_impl), make_dispatch_entry(&try_create_tensor_push_impl), make_dispatch_entry( &try_create_haptic_command_reader_impl), - make_dispatch_entry(&try_create_joint_state_impl), - make_dispatch_entry(&try_create_se3_tracker_impl), make_dispatch_entry(&try_create_oak_impl), - make_dispatch_entry(&try_create_oglo_impl), +// Manifest trackers are single-vendor, so their rows can sit last as a block. +#include "generated_live_dispatch_rows.inc" }; // Find a tracker's vendor selection in the config, or nullptr when unlisted. @@ -493,59 +462,11 @@ std::unique_ptr LiveDeviceIOFactory::create_full_body_trac return std::make_unique(handles_, *vendor, std::move(channels)); } -std::unique_ptr LiveDeviceIOFactory::create_generic_3axis_pedal_tracker_impl( - const Generic3AxisPedalTracker* tracker) -{ - std::unique_ptr channels; - if (should_record(tracker)) - { - channels = LiveGeneric3AxisPedalTrackerImpl::create_mcap_channels(*writer_, get_name(tracker)); - } - return std::make_unique(handles_, tracker, std::move(channels)); -} - -std::unique_ptr LiveDeviceIOFactory::create_oglo_tactile_tracker_impl( - const OgloTactileTracker* tracker) -{ - std::unique_ptr channels; - if (should_record(tracker)) - { - channels = LiveOgloTactileTrackerImpl::create_mcap_channels(*writer_, get_name(tracker)); - } - return std::make_unique(handles_, tracker, std::move(channels)); -} - std::unique_ptr LiveDeviceIOFactory::create_tensor_push_tracker_impl(const TensorPushTracker* tracker) { return std::make_unique(handles_, tracker); } -std::unique_ptr LiveDeviceIOFactory::create_haptic_command_reader_tracker_impl( - const HapticCommandReaderTracker* tracker) -{ - return std::make_unique(handles_, tracker); -} - -std::unique_ptr LiveDeviceIOFactory::create_joint_state_tracker_impl(const JointStateTracker* tracker) -{ - std::unique_ptr channels; - if (should_record(tracker)) - { - channels = LiveJointStateTrackerImpl::create_mcap_channels(*writer_, get_name(tracker)); - } - return std::make_unique(handles_, tracker, std::move(channels)); -} - -std::unique_ptr LiveDeviceIOFactory::create_se3_tracker_impl(const Se3Tracker* tracker) -{ - std::unique_ptr channels; - if (should_record(tracker)) - { - channels = LiveSe3TrackerImpl::create_mcap_channels(*writer_, get_name(tracker)); - } - return std::make_unique(handles_, tracker, std::move(channels)); -} - std::unique_ptr LiveDeviceIOFactory::create_frame_metadata_tracker_oak_impl( const FrameMetadataTrackerOak* tracker) { @@ -557,4 +478,12 @@ std::unique_ptr LiveDeviceIOFactory::create_frame_ return std::make_unique(handles_, tracker, std::move(channels)); } +std::unique_ptr LiveDeviceIOFactory::create_haptic_command_reader_tracker_impl( + const HapticCommandReaderTracker* tracker) +{ + return std::make_unique(handles_, tracker); +} + +#include "generated_live_factory_methods.inc" + } // namespace core diff --git a/src/core/live_trackers/cpp/live_generic_3axis_pedal_tracker_impl.cpp b/src/core/live_trackers/cpp/live_generic_3axis_pedal_tracker_impl.cpp deleted file mode 100644 index 3a4791856..000000000 --- a/src/core/live_trackers/cpp/live_generic_3axis_pedal_tracker_impl.cpp +++ /dev/null @@ -1,63 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -#include "live_generic_3axis_pedal_tracker_impl.hpp" - -#include -#include - -namespace core -{ - -namespace -{ - -SchemaTrackerConfig make_pedal_tensor_config(const Generic3AxisPedalTracker* tracker) -{ - SchemaTrackerConfig cfg; - cfg.collection_id = tracker->collection_id(); - cfg.max_flatbuffer_size = tracker->max_flatbuffer_size(); - cfg.tensor_identifier = "generic_3axis_pedal"; - cfg.localized_name = "Generic3AxisPedalTracker"; - return cfg; -} - -} // namespace - -// ============================================================================ -// LiveGeneric3AxisPedalTrackerImpl -// ============================================================================ - -std::unique_ptr LiveGeneric3AxisPedalTrackerImpl::create_mcap_channels(mcap::McapWriter& writer, - std::string_view base_name) -{ - return std::make_unique(writer, base_name, PedalRecordingTraits::schema_name, - std::vector(PedalRecordingTraits::recording_channels.begin(), - PedalRecordingTraits::recording_channels.end())); -} - -LiveGeneric3AxisPedalTrackerImpl::LiveGeneric3AxisPedalTrackerImpl(const OpenXRSessionHandles& handles, - const Generic3AxisPedalTracker* tracker, - std::unique_ptr mcap_channels) - : mcap_channels_(std::move(mcap_channels)), - m_schema_reader(handles, - make_pedal_tensor_config(tracker), - mcap_channels_.get(), - /*mcap_channel_index=*/0, - /*mcap_channel_tracked_index=*/1) -{ -} - -void LiveGeneric3AxisPedalTrackerImpl::update(int64_t /*monotonic_time_ns*/) -{ - // Policy: SchemaTracker throws on critical OpenXR/tensor API failures. - // Missing collection/no new data are treated as common non-fatal cases. - m_schema_reader.update(m_tracked.data); -} - -const Generic3AxisPedalOutputTrackedT& LiveGeneric3AxisPedalTrackerImpl::get_data() const -{ - return m_tracked; -} - -} // namespace core diff --git a/src/core/live_trackers/cpp/live_generic_3axis_pedal_tracker_impl.hpp b/src/core/live_trackers/cpp/live_generic_3axis_pedal_tracker_impl.hpp deleted file mode 100644 index f2d403591..000000000 --- a/src/core/live_trackers/cpp/live_generic_3axis_pedal_tracker_impl.hpp +++ /dev/null @@ -1,51 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2025 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 -#include - -namespace core -{ - -using PedalMcapChannels = McapTrackerChannels; -using PedalSchemaTracker = SchemaTracker; - -class LiveGeneric3AxisPedalTrackerImpl : public IGeneric3AxisPedalTrackerImpl -{ -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); - - LiveGeneric3AxisPedalTrackerImpl(const OpenXRSessionHandles& handles, - const Generic3AxisPedalTracker* tracker, - std::unique_ptr mcap_channels); - - LiveGeneric3AxisPedalTrackerImpl(const LiveGeneric3AxisPedalTrackerImpl&) = delete; - LiveGeneric3AxisPedalTrackerImpl& operator=(const LiveGeneric3AxisPedalTrackerImpl&) = delete; - LiveGeneric3AxisPedalTrackerImpl(LiveGeneric3AxisPedalTrackerImpl&&) = delete; - LiveGeneric3AxisPedalTrackerImpl& operator=(LiveGeneric3AxisPedalTrackerImpl&&) = delete; - - void update(int64_t monotonic_time_ns) override; - const Generic3AxisPedalOutputTrackedT& get_data() const override; - -private: - std::unique_ptr mcap_channels_; - PedalSchemaTracker m_schema_reader; - Generic3AxisPedalOutputTrackedT m_tracked; -}; - -} // namespace core diff --git a/src/core/live_trackers/cpp/live_joint_state_tracker_impl.cpp b/src/core/live_trackers/cpp/live_joint_state_tracker_impl.cpp deleted file mode 100644 index 6aa679810..000000000 --- a/src/core/live_trackers/cpp/live_joint_state_tracker_impl.cpp +++ /dev/null @@ -1,64 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -#include "live_joint_state_tracker_impl.hpp" - -#include -#include - -namespace core -{ - -namespace -{ - -SchemaTrackerConfig make_joint_state_tensor_config(const JointStateTracker* tracker) -{ - SchemaTrackerConfig cfg; - cfg.collection_id = tracker->collection_id(); - cfg.max_flatbuffer_size = tracker->max_flatbuffer_size(); - cfg.tensor_identifier = "joint_state"; - cfg.localized_name = "JointStateTracker"; - return cfg; -} - -} // namespace - -// ============================================================================ -// LiveJointStateTrackerImpl -// ============================================================================ - -std::unique_ptr LiveJointStateTrackerImpl::create_mcap_channels(mcap::McapWriter& writer, - std::string_view base_name) -{ - return std::make_unique( - writer, base_name, JointStateRecordingTraits::schema_name, - std::vector(JointStateRecordingTraits::recording_channels.begin(), - JointStateRecordingTraits::recording_channels.end())); -} - -LiveJointStateTrackerImpl::LiveJointStateTrackerImpl(const OpenXRSessionHandles& handles, - const JointStateTracker* tracker, - std::unique_ptr mcap_channels) - : mcap_channels_(std::move(mcap_channels)), - m_schema_reader(handles, - make_joint_state_tensor_config(tracker), - mcap_channels_.get(), - /*mcap_channel_index=*/0, - /*mcap_channel_tracked_index=*/1) -{ -} - -void LiveJointStateTrackerImpl::update(int64_t /*monotonic_time_ns*/) -{ - // Policy: SchemaTracker throws on critical OpenXR/tensor API failures. - // Missing collection/no new data are treated as common non-fatal cases. - m_schema_reader.update(m_tracked.data); -} - -const JointStateOutputTrackedT& LiveJointStateTrackerImpl::get_data() const -{ - return m_tracked; -} - -} // namespace core diff --git a/src/core/live_trackers/cpp/live_joint_state_tracker_impl.hpp b/src/core/live_trackers/cpp/live_joint_state_tracker_impl.hpp deleted file mode 100644 index b39a7e5ae..000000000 --- a/src/core/live_trackers/cpp/live_joint_state_tracker_impl.hpp +++ /dev/null @@ -1,52 +0,0 @@ -// 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 -#include - -namespace core -{ - -using JointStateMcapChannels = McapTrackerChannels; -using JointStateSchemaTracker = SchemaTracker; - -class LiveJointStateTrackerImpl : public IJointStateTrackerImpl -{ -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); - - LiveJointStateTrackerImpl(const OpenXRSessionHandles& handles, - const JointStateTracker* tracker, - std::unique_ptr mcap_channels); - - LiveJointStateTrackerImpl(const LiveJointStateTrackerImpl&) = delete; - LiveJointStateTrackerImpl& operator=(const LiveJointStateTrackerImpl&) = delete; - LiveJointStateTrackerImpl(LiveJointStateTrackerImpl&&) = delete; - LiveJointStateTrackerImpl& operator=(LiveJointStateTrackerImpl&&) = delete; - - void update(int64_t monotonic_time_ns) override; - const JointStateOutputTrackedT& get_data() const override; - -private: - std::unique_ptr mcap_channels_; - JointStateSchemaTracker m_schema_reader; - JointStateOutputTrackedT m_tracked; -}; - -} // namespace core diff --git a/src/core/live_trackers/cpp/live_oglo_tactile_tracker_impl.cpp b/src/core/live_trackers/cpp/live_oglo_tactile_tracker_impl.cpp deleted file mode 100644 index eb7fa2def..000000000 --- a/src/core/live_trackers/cpp/live_oglo_tactile_tracker_impl.cpp +++ /dev/null @@ -1,63 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -#include "live_oglo_tactile_tracker_impl.hpp" - -#include -#include - -namespace core -{ - -namespace -{ - -SchemaTrackerConfig make_oglo_tensor_config(const OgloTactileTracker* tracker) -{ - SchemaTrackerConfig cfg; - cfg.collection_id = tracker->collection_id(); - cfg.max_flatbuffer_size = tracker->max_flatbuffer_size(); - cfg.tensor_identifier = "oglo_tactile"; - cfg.localized_name = "OgloTactileTracker"; - return cfg; -} - -} // namespace - -// ============================================================================ -// LiveOgloTactileTrackerImpl -// ============================================================================ - -std::unique_ptr LiveOgloTactileTrackerImpl::create_mcap_channels(mcap::McapWriter& writer, - std::string_view base_name) -{ - return std::make_unique(writer, base_name, OgloRecordingTraits::schema_name, - std::vector(OgloRecordingTraits::recording_channels.begin(), - OgloRecordingTraits::recording_channels.end())); -} - -LiveOgloTactileTrackerImpl::LiveOgloTactileTrackerImpl(const OpenXRSessionHandles& handles, - const OgloTactileTracker* tracker, - std::unique_ptr mcap_channels) - : mcap_channels_(std::move(mcap_channels)), - m_schema_reader(handles, - make_oglo_tensor_config(tracker), - mcap_channels_.get(), - /*mcap_channel_index=*/0, - /*mcap_channel_tracked_index=*/1) -{ -} - -void LiveOgloTactileTrackerImpl::update(int64_t /*monotonic_time_ns*/) -{ - // SchemaTracker throws on critical OpenXR/tensor failures; missing collection - // and "no new data" are non-fatal. - m_schema_reader.update(m_tracked.data); -} - -const OgloGloveSampleTrackedT& LiveOgloTactileTrackerImpl::get_data() const -{ - return m_tracked; -} - -} // namespace core diff --git a/src/core/live_trackers/cpp/live_oglo_tactile_tracker_impl.hpp b/src/core/live_trackers/cpp/live_oglo_tactile_tracker_impl.hpp deleted file mode 100644 index 57e3362c9..000000000 --- a/src/core/live_trackers/cpp/live_oglo_tactile_tracker_impl.hpp +++ /dev/null @@ -1,51 +0,0 @@ -// 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 -#include - -namespace core -{ - -using OgloMcapChannels = McapTrackerChannels; -using OgloSchemaTracker = SchemaTracker; - -class LiveOgloTactileTrackerImpl : public IOgloTactileTrackerImpl -{ -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); - - LiveOgloTactileTrackerImpl(const OpenXRSessionHandles& handles, - const OgloTactileTracker* tracker, - std::unique_ptr mcap_channels); - - LiveOgloTactileTrackerImpl(const LiveOgloTactileTrackerImpl&) = delete; - LiveOgloTactileTrackerImpl& operator=(const LiveOgloTactileTrackerImpl&) = delete; - LiveOgloTactileTrackerImpl(LiveOgloTactileTrackerImpl&&) = delete; - LiveOgloTactileTrackerImpl& operator=(LiveOgloTactileTrackerImpl&&) = delete; - - void update(int64_t monotonic_time_ns) override; - const OgloGloveSampleTrackedT& get_data() const override; - -private: - std::unique_ptr mcap_channels_; - OgloSchemaTracker m_schema_reader; - OgloGloveSampleTrackedT m_tracked; -}; - -} // namespace core diff --git a/src/core/live_trackers/cpp/live_se3_tracker_impl.cpp b/src/core/live_trackers/cpp/live_se3_tracker_impl.cpp deleted file mode 100644 index fd211743a..000000000 --- a/src/core/live_trackers/cpp/live_se3_tracker_impl.cpp +++ /dev/null @@ -1,67 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -#include "live_se3_tracker_impl.hpp" - -#include -#include - -namespace core -{ - -namespace -{ - -SchemaTrackerConfig make_se3_tracker_tensor_config(const Se3Tracker* tracker) -{ - SchemaTrackerConfig cfg; - cfg.collection_id = tracker->collection_id(); - cfg.max_flatbuffer_size = tracker->max_flatbuffer_size(); - // Wire rendezvous with producer plugins; single source of truth on the facade. - cfg.tensor_identifier = std::string(Se3Tracker::TENSOR_IDENTIFIER); - cfg.localized_name = "Se3Tracker"; - return cfg; -} - -} // namespace - -// ============================================================================ -// LiveSe3TrackerImpl -// ============================================================================ - -std::unique_ptr LiveSe3TrackerImpl::create_mcap_channels(mcap::McapWriter& writer, - std::string_view base_name) -{ - return std::make_unique( - writer, base_name, Se3TrackerRecordingTraits::schema_name, - std::vector(Se3TrackerRecordingTraits::recording_channels.begin(), - Se3TrackerRecordingTraits::recording_channels.end())); -} - -LiveSe3TrackerImpl::LiveSe3TrackerImpl(const OpenXRSessionHandles& handles, - const Se3Tracker* tracker, - std::unique_ptr mcap_channels) - : mcap_channels_(std::move(mcap_channels)), - // Channel indices follow Se3TrackerRecordingTraits::recording_channels order: - // 0 = per-sample "se3_tracker", 1 = per-tick "se3_tracker_tracked". - m_schema_reader(handles, - make_se3_tracker_tensor_config(tracker), - mcap_channels_.get(), - /*mcap_channel_index=*/0, - /*mcap_channel_tracked_index=*/1) -{ -} - -void LiveSe3TrackerImpl::update(int64_t /*monotonic_time_ns*/) -{ - // Policy: SchemaTracker throws on critical OpenXR/tensor API failures. - // Missing collection/no new data are treated as common non-fatal cases. - m_schema_reader.update(m_tracked.data); -} - -const Se3TrackerPoseTrackedT& LiveSe3TrackerImpl::get_data() const -{ - return m_tracked; -} - -} // namespace core diff --git a/src/core/live_trackers/cpp/live_se3_tracker_impl.hpp b/src/core/live_trackers/cpp/live_se3_tracker_impl.hpp deleted file mode 100644 index 2286d0e5a..000000000 --- a/src/core/live_trackers/cpp/live_se3_tracker_impl.hpp +++ /dev/null @@ -1,52 +0,0 @@ -// 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 -#include - -namespace core -{ - -using Se3TrackerMcapChannels = McapTrackerChannels; -using Se3TrackerSchemaTracker = SchemaTracker; - -class LiveSe3TrackerImpl : public ISe3TrackerImpl -{ -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); - - LiveSe3TrackerImpl(const OpenXRSessionHandles& handles, - const Se3Tracker* tracker, - std::unique_ptr mcap_channels); - - LiveSe3TrackerImpl(const LiveSe3TrackerImpl&) = delete; - LiveSe3TrackerImpl& operator=(const LiveSe3TrackerImpl&) = delete; - LiveSe3TrackerImpl(LiveSe3TrackerImpl&&) = delete; - LiveSe3TrackerImpl& operator=(LiveSe3TrackerImpl&&) = delete; - - void update(int64_t monotonic_time_ns) override; - const Se3TrackerPoseTrackedT& get_data() const override; - -private: - std::unique_ptr mcap_channels_; - Se3TrackerSchemaTracker m_schema_reader; - Se3TrackerPoseTrackedT m_tracked; -}; - -} // namespace core diff --git a/src/core/mcap/cpp/CMakeLists.txt b/src/core/mcap/cpp/CMakeLists.txt index 2b8e62d37..2b30b29e0 100644 --- a/src/core/mcap/cpp/CMakeLists.txt +++ b/src/core/mcap/cpp/CMakeLists.txt @@ -13,6 +13,7 @@ add_library(mcap_core INTERFACE) target_include_directories(mcap_core INTERFACE $ + $ ) # Transitively expose mcap::mcap so consumers get diff --git a/src/core/mcap/cpp/inc/mcap/recording_traits.hpp b/src/core/mcap/cpp/inc/mcap/recording_traits.hpp index 4e5b61b4a..9631a7f51 100644 --- a/src/core/mcap/cpp/inc/mcap/recording_traits.hpp +++ b/src/core/mcap/cpp/inc/mcap/recording_traits.hpp @@ -51,33 +51,9 @@ struct FullBodyRecordingTraits // FullBodyRecordingTraits. using FullBodyPicoRecordingTraits [[deprecated("renamed to core::FullBodyRecordingTraits")]] = FullBodyRecordingTraits; -struct PedalRecordingTraits -{ - static constexpr std::string_view schema_name = "core.Generic3AxisPedalOutputRecord"; - static constexpr std::array recording_channels = { "pedals", "pedals_tracked" }; - static constexpr std::array replay_channels = { "pedals_tracked" }; -}; - -struct OgloRecordingTraits -{ - static constexpr std::string_view schema_name = "core.OgloGloveSampleRecord"; - static constexpr std::array recording_channels = { "oglo", "oglo_tracked" }; - static constexpr std::array replay_channels = { "oglo_tracked" }; -}; - -struct JointStateRecordingTraits -{ - static constexpr std::string_view schema_name = "core.JointStateOutputRecord"; - static constexpr std::array recording_channels = { "joint_state", "joint_state_tracked" }; - static constexpr std::array replay_channels = { "joint_state_tracked" }; -}; - -struct Se3TrackerRecordingTraits -{ - static constexpr std::string_view schema_name = "core.Se3TrackerPoseRecord"; - static constexpr std::array recording_channels = { "se3_tracker", "se3_tracker_tracked" }; - static constexpr std::array replay_channels = { "se3_tracker_tracked" }; -}; +// Traits for trackers declared in deviceio_trackers/trackers.toml, emitted from their +// channel/schema_name manifest keys. Add traits above by hand only for hand-written trackers. +#include "generated_recording_traits.inc" struct OakRecordingTraits { diff --git a/src/core/python/CMakeLists.txt b/src/core/python/CMakeLists.txt index dcb9917a1..b8cc4f66d 100644 --- a/src/core/python/CMakeLists.txt +++ b/src/core/python/CMakeLists.txt @@ -31,6 +31,15 @@ configure_file( @ONLY ) +# Configure-time tracker codegen: splice manifest tracker names into deviceio_trackers. +# Staged before python_package (and therefore before central stubgen, which DEPENDS on +# python_package) so isaacteleop.deviceio_trackers can import the export module. +add_custom_target(stage_generated_tracker_exports ALL + COMMAND ${CMAKE_COMMAND} -E make_directory "${CMAKE_BINARY_DIR}/python_package/$/isaacteleop/deviceio_trackers" + COMMAND ${CMAKE_COMMAND} -E copy "${GENERATED_TRACKER_DIR}/python/_generated_tracker_exports.py" "${CMAKE_BINARY_DIR}/python_package/$/isaacteleop/deviceio_trackers/_generated_tracker_exports.py" + COMMENT "Stage _generated_tracker_exports.py for deviceio_trackers" +) + # After building modules, create the Python package structure. add_custom_target(python_package ALL # The isaacteleop/ subtree itself is staged by src/python (pure Python), @@ -50,7 +59,7 @@ add_custom_target(python_package ALL COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/requirements-retargeters-lite.txt" "${CMAKE_BINARY_DIR}/python_package/$/" COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/requirements-grounding.txt" "${CMAKE_BINARY_DIR}/python_package/$/" COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_SOURCE_DIR}/requirements-wuji.txt" "${CMAKE_BINARY_DIR}/python_package/$/" - DEPENDS deviceio_trackers_py deviceio_session_py oxr_py plugin_manager_py schema_py isaacteleop_python cloudxr_python + DEPENDS deviceio_trackers_py deviceio_session_py oxr_py plugin_manager_py schema_py isaacteleop_python cloudxr_python stage_generated_tracker_exports COMMENT "Preparing Python package structure" ) @@ -207,6 +216,16 @@ if(SKBUILD) # mapping and silently serve the stale staged copy -- editing src/python # would appear to do nothing. `wheel.exclude` cannot substitute: scikit-build- # core documents that editable installs may not respect it. + # + # Exception: configure-time codegen emits _generated_tracker_exports.py (not + # under src/python/). deviceio_trackers/__init__.py star-imports it; without + # this install rule editable `pip install -e .` resolves __init__.py from + # source and the export module is missing. + install( + FILES "${CMAKE_BINARY_DIR}/python_package/${_it_wheel_config}/isaacteleop/deviceio_trackers/_generated_tracker_exports.py" + DESTINATION "isaacteleop/deviceio_trackers" + COMPONENT isaacteleop_wheel + ) install( DIRECTORY "${CMAKE_BINARY_DIR}/python_package/${_it_wheel_config}/isaacteleop" DESTINATION "." diff --git a/src/core/replay_trackers/AGENTS.md b/src/core/replay_trackers/AGENTS.md new file mode 100644 index 000000000..e0432e905 --- /dev/null +++ b/src/core/replay_trackers/AGENTS.md @@ -0,0 +1,36 @@ + + +# Agent notes — `replay_trackers` + +**CRITICAL (non-optional):** Before editing this package, complete the mandatory **`AGENTS.md` preflight** in [`../../../AGENTS.md`](../../../AGENTS.md) (read every applicable `AGENTS.md` on your paths, not just this file). + +## Half this package is generated + +Replay impls for trackers declared in +[`../deviceio_trackers/trackers.toml`](../deviceio_trackers/trackers.toml) are emitted into +`${CMAKE_BINARY_DIR}/generated/trackers/replay_trackers/`. Only the hand-written trackers +(`head`, `hand`, `controller`, `full_body`, `message_channel`, `TensorPushTracker`) have `.cpp` +files in this directory. + +- `replay_deviceio_factory.{hpp,cpp}` stays hand-written but `#include`s generated `.inc` fragments + for the manifest trackers' forward decls, try-create thunks, dispatch rows, and factory methods. + **Do not** add rows for a manifest tracker by hand. +- The forward-decl fragment is shared with the live factory header — it is + `generated_tracker_forward_decls.inc`, not a replay-specific one. Keep it direction-agnostic. + +## Missing-data logging is warn-once, by design + +A replay impl reaching EOF or a gap is called **every frame**. Log the "no data" message once per +gap behind a `warned_no_data_` flag and reset the flag when a record arrives — never write to +`std::cerr` unconditionally in `update()`. The generated impls do this; hand-written ones must too. +This unification is why the generated replay stack is not a byte-for-byte port of the older +per-tracker files. + +## Related docs + +- Manifest and generator rules: [`../codegen/AGENTS.md`](../codegen/AGENTS.md) +- Replay session lifecycle: [`../deviceio_session/AGENTS.md`](../deviceio_session/AGENTS.md) +- Live counterpart: [`../live_trackers/AGENTS.md`](../live_trackers/AGENTS.md) diff --git a/src/core/replay_trackers/cpp/CMakeLists.txt b/src/core/replay_trackers/cpp/CMakeLists.txt index f87dc13d8..d3347127c 100644 --- a/src/core/replay_trackers/cpp/CMakeLists.txt +++ b/src/core/replay_trackers/cpp/CMakeLists.txt @@ -9,30 +9,26 @@ add_library(replay_trackers STATIC replay_head_tracker_impl.cpp replay_controller_tracker_impl.cpp replay_full_body_tracker_impl.cpp - replay_generic_3axis_pedal_tracker_impl.cpp - replay_oglo_tactile_tracker_impl.cpp - replay_joint_state_tracker_impl.cpp - replay_se3_tracker_impl.cpp replay_message_channel_tracker_impl.cpp replay_tensor_push_tracker_impl.cpp replay_haptic_command_reader_tracker_impl.cpp + ${GENERATED_TRACKER_REPLAY_SOURCES} inc/replay_trackers/replay_deviceio_factory.hpp replay_hand_tracker_impl.hpp replay_head_tracker_impl.hpp replay_controller_tracker_impl.hpp replay_full_body_tracker_impl.hpp - replay_generic_3axis_pedal_tracker_impl.hpp - replay_oglo_tactile_tracker_impl.hpp - replay_joint_state_tracker_impl.hpp - replay_se3_tracker_impl.hpp replay_message_channel_tracker_impl.hpp replay_tensor_push_tracker_impl.hpp replay_haptic_command_reader_tracker_impl.hpp ) target_include_directories(replay_trackers - INTERFACE + PUBLIC $ + $ + $ + $ ) target_link_libraries(replay_trackers 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 e231f0c8a..f4ecd8d35 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,54 +19,38 @@ class ControllerTracker; class IControllerTrackerImpl; class FullBodyTracker; class IFullBodyTrackerImpl; -class Generic3AxisPedalTracker; -class IGeneric3AxisPedalTrackerImpl; -class OgloTactileTracker; -class IOgloTactileTrackerImpl; class TensorPushTracker; class ITensorPushTrackerImpl; -class HapticCommandReaderTracker; -class IHapticCommandReaderTrackerImpl; -class JointStateTracker; -class IJointStateTrackerImpl; -class Se3Tracker; -class ISe3TrackerImpl; class HandTracker; class IHandTrackerImpl; class HeadTracker; class IHeadTrackerImpl; +class HapticCommandReaderTracker; +class IHapticCommandReaderTrackerImpl; class MessageChannelTracker; class IMessageChannelTrackerImpl; -/** - * @brief Factory for replay (MCAP-backed) tracker implementations. - * - * Opens a fresh McapReader per tracker impl so each tracker has its own - * FileReader buffer; crossing an MCAP chunk boundary in one tracker cannot - * overwrite another tracker's pre-fetched message data pointer. - */ +// Same fragment the live factory header uses: forward decls are direction-agnostic. +#include "generated_tracker_forward_decls.inc" + class ReplayDeviceIOFactory { public: ReplayDeviceIOFactory(std::string filename, const std::vector>& tracker_names); - /** Create tracker impl from a tracker instance using dynamic dispatch. */ std::unique_ptr create_tracker_impl(const ITracker& tracker); std::unique_ptr create_head_tracker_impl(const HeadTracker* tracker); 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_impl(const FullBodyTracker* tracker); - std::unique_ptr create_generic_3axis_pedal_tracker_impl( - const Generic3AxisPedalTracker* tracker); - std::unique_ptr create_oglo_tactile_tracker_impl(const OgloTactileTracker* tracker); std::unique_ptr create_tensor_push_tracker_impl(const TensorPushTracker* tracker); + std::unique_ptr create_message_channel_tracker_impl(const MessageChannelTracker* tracker); std::unique_ptr create_haptic_command_reader_tracker_impl( const HapticCommandReaderTracker* tracker); - std::unique_ptr create_joint_state_tracker_impl(const JointStateTracker* tracker); - std::unique_ptr create_se3_tracker_impl(const Se3Tracker* tracker); - std::unique_ptr create_message_channel_tracker_impl(const MessageChannelTracker* tracker); + // create__tracker_impl for every manifest tracker. +#include "generated_replay_factory_declarations.inc" private: std::string_view get_name(const ITracker* tracker) const; diff --git a/src/core/replay_trackers/cpp/replay_deviceio_factory.cpp b/src/core/replay_trackers/cpp/replay_deviceio_factory.cpp index 7a6981126..563bedfdd 100644 --- a/src/core/replay_trackers/cpp/replay_deviceio_factory.cpp +++ b/src/core/replay_trackers/cpp/replay_deviceio_factory.cpp @@ -3,28 +3,21 @@ #include "inc/replay_trackers/replay_deviceio_factory.hpp" +#include "generated_replay_includes.inc" #include "replay_controller_tracker_impl.hpp" #include "replay_full_body_tracker_impl.hpp" -#include "replay_generic_3axis_pedal_tracker_impl.hpp" #include "replay_hand_tracker_impl.hpp" #include "replay_haptic_command_reader_tracker_impl.hpp" #include "replay_head_tracker_impl.hpp" -#include "replay_joint_state_tracker_impl.hpp" #include "replay_message_channel_tracker_impl.hpp" -#include "replay_oglo_tactile_tracker_impl.hpp" -#include "replay_se3_tracker_impl.hpp" #include "replay_tensor_push_tracker_impl.hpp" #include #include -#include #include #include #include -#include #include -#include -#include #include #include @@ -50,7 +43,6 @@ std::unique_ptr open_reader(const std::string& filename) return reader; } - std::unique_ptr try_create_head_impl(ReplayDeviceIOFactory& factory, const ITracker& tracker) { auto* typed = dynamic_cast(&tracker); @@ -75,24 +67,18 @@ std::unique_ptr try_create_full_body_impl(ReplayDeviceIOFactory& f return typed ? factory.create_full_body_tracker_impl(typed) : nullptr; } -std::unique_ptr try_create_generic_pedal_impl(ReplayDeviceIOFactory& factory, const ITracker& tracker) -{ - auto* typed = dynamic_cast(&tracker); - return typed ? factory.create_generic_3axis_pedal_tracker_impl(typed) : nullptr; -} - -std::unique_ptr try_create_oglo_impl(ReplayDeviceIOFactory& factory, const ITracker& tracker) -{ - auto* typed = dynamic_cast(&tracker); - return typed ? factory.create_oglo_tactile_tracker_impl(typed) : nullptr; -} - std::unique_ptr try_create_tensor_push_impl(ReplayDeviceIOFactory& factory, const ITracker& tracker) { auto* typed = dynamic_cast(&tracker); return typed ? factory.create_tensor_push_tracker_impl(typed) : nullptr; } +std::unique_ptr try_create_message_channel_impl(ReplayDeviceIOFactory& factory, const ITracker& tracker) +{ + auto* typed = dynamic_cast(&tracker); + return typed ? factory.create_message_channel_tracker_impl(typed) : nullptr; +} + std::unique_ptr try_create_haptic_command_reader_impl(ReplayDeviceIOFactory& factory, const ITracker& tracker) { @@ -100,23 +86,7 @@ std::unique_ptr try_create_haptic_command_reader_impl(ReplayDevice return typed ? factory.create_haptic_command_reader_tracker_impl(typed) : nullptr; } -std::unique_ptr try_create_joint_state_impl(ReplayDeviceIOFactory& factory, const ITracker& tracker) -{ - auto* typed = dynamic_cast(&tracker); - return typed ? factory.create_joint_state_tracker_impl(typed) : nullptr; -} - -std::unique_ptr try_create_se3_tracker_impl(ReplayDeviceIOFactory& factory, const ITracker& tracker) -{ - auto* typed = dynamic_cast(&tracker); - return typed ? factory.create_se3_tracker_impl(typed) : nullptr; -} - -std::unique_ptr try_create_message_channel_impl(ReplayDeviceIOFactory& factory, const ITracker& tracker) -{ - auto* typed = dynamic_cast(&tracker); - return typed ? factory.create_message_channel_tracker_impl(typed) : nullptr; -} +#include "generated_replay_try_create.inc" using TryCreateFn = std::unique_ptr (*)(ReplayDeviceIOFactory&, const ITracker&); @@ -125,12 +95,9 @@ inline const TryCreateFn k_tracker_dispatch[] = { &try_create_hand_impl, &try_create_controller_impl, &try_create_full_body_impl, - &try_create_generic_pedal_impl, - &try_create_oglo_impl, - &try_create_tensor_push_impl, +#include "generated_replay_dispatch_rows.inc" &try_create_haptic_command_reader_impl, - &try_create_joint_state_impl, - &try_create_se3_tracker_impl, + &try_create_tensor_push_impl, &try_create_message_channel_impl, }; @@ -191,44 +158,24 @@ std::unique_ptr ReplayDeviceIOFactory::create_full_body_tr return std::make_unique(open_reader(filename_), get_name(tracker)); } -std::unique_ptr ReplayDeviceIOFactory::create_generic_3axis_pedal_tracker_impl( - const Generic3AxisPedalTracker* tracker) -{ - return std::make_unique(open_reader(filename_), get_name(tracker)); -} - -std::unique_ptr ReplayDeviceIOFactory::create_oglo_tactile_tracker_impl( - const OgloTactileTracker* tracker) -{ - return std::make_unique(open_reader(filename_), get_name(tracker)); -} - std::unique_ptr ReplayDeviceIOFactory::create_tensor_push_tracker_impl( const TensorPushTracker* /*tracker*/) { return std::make_unique(); } -std::unique_ptr ReplayDeviceIOFactory::create_haptic_command_reader_tracker_impl( - const HapticCommandReaderTracker* /*tracker*/) -{ - return std::make_unique(); -} - -std::unique_ptr ReplayDeviceIOFactory::create_joint_state_tracker_impl(const JointStateTracker* tracker) +std::unique_ptr ReplayDeviceIOFactory::create_message_channel_tracker_impl( + const MessageChannelTracker* tracker) { - return std::make_unique(open_reader(filename_), get_name(tracker)); + return std::make_unique(open_reader(filename_), get_name(tracker)); } -std::unique_ptr ReplayDeviceIOFactory::create_se3_tracker_impl(const Se3Tracker* tracker) +std::unique_ptr ReplayDeviceIOFactory::create_haptic_command_reader_tracker_impl( + const HapticCommandReaderTracker* /*tracker*/) { - return std::make_unique(open_reader(filename_), get_name(tracker)); + return std::make_unique(); } -std::unique_ptr ReplayDeviceIOFactory::create_message_channel_tracker_impl( - const MessageChannelTracker* tracker) -{ - return std::make_unique(open_reader(filename_), get_name(tracker)); -} +#include "generated_replay_factory_methods.inc" } // namespace core diff --git a/src/core/replay_trackers/cpp/replay_generic_3axis_pedal_tracker_impl.cpp b/src/core/replay_trackers/cpp/replay_generic_3axis_pedal_tracker_impl.cpp deleted file mode 100644 index 9a6c99d9b..000000000 --- a/src/core/replay_trackers/cpp/replay_generic_3axis_pedal_tracker_impl.cpp +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -#include "replay_generic_3axis_pedal_tracker_impl.hpp" - -#include -#include -#include - -#include -#include -#include - -namespace core -{ - -// ============================================================================ -// ReplayGeneric3AxisPedalTrackerImpl -// ============================================================================ - -ReplayGeneric3AxisPedalTrackerImpl::ReplayGeneric3AxisPedalTrackerImpl(std::unique_ptr reader, - std::string_view base_name) - : mcap_viewers_( - std::make_unique(std::move(reader), - base_name, - std::vector(PedalRecordingTraits::replay_channels.begin(), - PedalRecordingTraits::replay_channels.end()))) -{ -} - -const Generic3AxisPedalOutputTrackedT& ReplayGeneric3AxisPedalTrackerImpl::get_data() const -{ - return tracked_; -} - -void ReplayGeneric3AxisPedalTrackerImpl::update(int64_t /*monotonic_time_ns*/) -{ - auto record = mcap_viewers_->read(0); - if (record) - { - tracked_.data = std::move(record->data); - } - else - { - std::cerr << "ReplayGeneric3AxisPedalTrackerImpl: pedal data not found" << std::endl; - tracked_.data.reset(); - } -} - -} // namespace core diff --git a/src/core/replay_trackers/cpp/replay_generic_3axis_pedal_tracker_impl.hpp b/src/core/replay_trackers/cpp/replay_generic_3axis_pedal_tracker_impl.hpp deleted file mode 100644 index 8974cc234..000000000 --- a/src/core/replay_trackers/cpp/replay_generic_3axis_pedal_tracker_impl.hpp +++ /dev/null @@ -1,37 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -#pragma once - -#include -#include -#include - -#include -#include -#include - -namespace core -{ - -using PedalMcapViewers = McapTrackerViewers; - -class ReplayGeneric3AxisPedalTrackerImpl : public IGeneric3AxisPedalTrackerImpl -{ -public: - ReplayGeneric3AxisPedalTrackerImpl(std::unique_ptr reader, std::string_view base_name); - - ReplayGeneric3AxisPedalTrackerImpl(const ReplayGeneric3AxisPedalTrackerImpl&) = delete; - ReplayGeneric3AxisPedalTrackerImpl& operator=(const ReplayGeneric3AxisPedalTrackerImpl&) = delete; - ReplayGeneric3AxisPedalTrackerImpl(ReplayGeneric3AxisPedalTrackerImpl&&) = delete; - ReplayGeneric3AxisPedalTrackerImpl& operator=(ReplayGeneric3AxisPedalTrackerImpl&&) = delete; - - void update(int64_t monotonic_time_ns) override; - const Generic3AxisPedalOutputTrackedT& get_data() const override; - -private: - Generic3AxisPedalOutputTrackedT tracked_; - std::unique_ptr mcap_viewers_; -}; - -} // namespace core diff --git a/src/core/replay_trackers/cpp/replay_joint_state_tracker_impl.cpp b/src/core/replay_trackers/cpp/replay_joint_state_tracker_impl.cpp deleted file mode 100644 index 00567e051..000000000 --- a/src/core/replay_trackers/cpp/replay_joint_state_tracker_impl.cpp +++ /dev/null @@ -1,57 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -#include "replay_joint_state_tracker_impl.hpp" - -#include -#include -#include - -#include -#include -#include -#include - -namespace core -{ - -// ============================================================================ -// ReplayJointStateTrackerImpl -// ============================================================================ - -ReplayJointStateTrackerImpl::ReplayJointStateTrackerImpl(std::unique_ptr reader, - std::string_view base_name) - : mcap_viewers_(std::make_unique( - std::move(reader), - base_name, - std::vector( - JointStateRecordingTraits::replay_channels.begin(), JointStateRecordingTraits::replay_channels.end()))) -{ -} - -const JointStateOutputTrackedT& ReplayJointStateTrackerImpl::get_data() const -{ - return tracked_; -} - -void ReplayJointStateTrackerImpl::update(int64_t /*monotonic_time_ns*/) -{ - auto record = mcap_viewers_->read(0); - if (record) - { - tracked_.data = std::move(record->data); - warned_no_data_ = false; - } - else - { - // EOF / sparse streams call this every frame; log once per gap, not per frame. - if (!warned_no_data_) - { - std::cerr << "ReplayJointStateTrackerImpl: joint state data not found" << std::endl; - warned_no_data_ = true; - } - tracked_.data.reset(); - } -} - -} // namespace core diff --git a/src/core/replay_trackers/cpp/replay_joint_state_tracker_impl.hpp b/src/core/replay_trackers/cpp/replay_joint_state_tracker_impl.hpp deleted file mode 100644 index 63dd9e174..000000000 --- a/src/core/replay_trackers/cpp/replay_joint_state_tracker_impl.hpp +++ /dev/null @@ -1,39 +0,0 @@ -// 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 JointStateMcapViewers = McapTrackerViewers; - -class ReplayJointStateTrackerImpl : public IJointStateTrackerImpl -{ -public: - ReplayJointStateTrackerImpl(std::unique_ptr reader, std::string_view base_name); - - ReplayJointStateTrackerImpl(const ReplayJointStateTrackerImpl&) = delete; - ReplayJointStateTrackerImpl& operator=(const ReplayJointStateTrackerImpl&) = delete; - ReplayJointStateTrackerImpl(ReplayJointStateTrackerImpl&&) = delete; - ReplayJointStateTrackerImpl& operator=(ReplayJointStateTrackerImpl&&) = delete; - - void update(int64_t monotonic_time_ns) override; - const JointStateOutputTrackedT& get_data() const override; - -private: - JointStateOutputTrackedT tracked_; - std::unique_ptr mcap_viewers_; - // Warn only on the first frame of a no-data gap (EOF / sparse stream) to avoid per-frame spam. - bool warned_no_data_ = false; -}; - -} // namespace core diff --git a/src/core/replay_trackers/cpp/replay_oglo_tactile_tracker_impl.cpp b/src/core/replay_trackers/cpp/replay_oglo_tactile_tracker_impl.cpp deleted file mode 100644 index 4310ba771..000000000 --- a/src/core/replay_trackers/cpp/replay_oglo_tactile_tracker_impl.cpp +++ /dev/null @@ -1,50 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -#include "replay_oglo_tactile_tracker_impl.hpp" - -#include -#include -#include - -#include -#include -#include - -namespace core -{ - -// ============================================================================ -// ReplayOgloTactileTrackerImpl -// ============================================================================ - -ReplayOgloTactileTrackerImpl::ReplayOgloTactileTrackerImpl(std::unique_ptr reader, - std::string_view base_name) - : mcap_viewers_( - std::make_unique(std::move(reader), - base_name, - std::vector(OgloRecordingTraits::replay_channels.begin(), - OgloRecordingTraits::replay_channels.end()))) -{ -} - -const OgloGloveSampleTrackedT& ReplayOgloTactileTrackerImpl::get_data() const -{ - return tracked_; -} - -void ReplayOgloTactileTrackerImpl::update(int64_t /*monotonic_time_ns*/) -{ - auto record = mcap_viewers_->read(0); - if (record) - { - tracked_.data = std::move(record->data); - } - else - { - std::cerr << "ReplayOgloTactileTrackerImpl: glove data not found" << std::endl; - tracked_.data.reset(); - } -} - -} // namespace core diff --git a/src/core/replay_trackers/cpp/replay_oglo_tactile_tracker_impl.hpp b/src/core/replay_trackers/cpp/replay_oglo_tactile_tracker_impl.hpp deleted file mode 100644 index 11a84a1b2..000000000 --- a/src/core/replay_trackers/cpp/replay_oglo_tactile_tracker_impl.hpp +++ /dev/null @@ -1,37 +0,0 @@ -// 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 OgloMcapViewers = McapTrackerViewers; - -class ReplayOgloTactileTrackerImpl : public IOgloTactileTrackerImpl -{ -public: - ReplayOgloTactileTrackerImpl(std::unique_ptr reader, std::string_view base_name); - - ReplayOgloTactileTrackerImpl(const ReplayOgloTactileTrackerImpl&) = delete; - ReplayOgloTactileTrackerImpl& operator=(const ReplayOgloTactileTrackerImpl&) = delete; - ReplayOgloTactileTrackerImpl(ReplayOgloTactileTrackerImpl&&) = delete; - ReplayOgloTactileTrackerImpl& operator=(ReplayOgloTactileTrackerImpl&&) = delete; - - void update(int64_t monotonic_time_ns) override; - const OgloGloveSampleTrackedT& get_data() const override; - -private: - OgloGloveSampleTrackedT tracked_; - std::unique_ptr mcap_viewers_; -}; - -} // namespace core diff --git a/src/core/replay_trackers/cpp/replay_se3_tracker_impl.cpp b/src/core/replay_trackers/cpp/replay_se3_tracker_impl.cpp deleted file mode 100644 index a88faa707..000000000 --- a/src/core/replay_trackers/cpp/replay_se3_tracker_impl.cpp +++ /dev/null @@ -1,57 +0,0 @@ -// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. -// SPDX-License-Identifier: Apache-2.0 - -#include "replay_se3_tracker_impl.hpp" - -#include -#include -#include - -#include -#include -#include -#include - -namespace core -{ - -// ============================================================================ -// ReplaySe3TrackerImpl -// ============================================================================ - -ReplaySe3TrackerImpl::ReplaySe3TrackerImpl(std::unique_ptr reader, std::string_view base_name) - : mcap_viewers_(std::make_unique( - std::move(reader), - base_name, - std::vector( - Se3TrackerRecordingTraits::replay_channels.begin(), Se3TrackerRecordingTraits::replay_channels.end()))), - no_data_message_("ReplaySe3TrackerImpl[" + std::string(base_name) + "]: no data (EOF or gap)") -{ -} - -const Se3TrackerPoseTrackedT& ReplaySe3TrackerImpl::get_data() const -{ - return tracked_; -} - -void ReplaySe3TrackerImpl::update(int64_t /*monotonic_time_ns*/) -{ - auto record = mcap_viewers_->read(0); - if (record) - { - tracked_.data = std::move(record->data); - warned_no_data_ = false; - } - else - { - // EOF / sparse streams call this every frame; log once per gap, not per frame. - if (!warned_no_data_) - { - std::cerr << no_data_message_ << std::endl; - warned_no_data_ = true; - } - tracked_.data.reset(); - } -} - -} // namespace core diff --git a/src/core/replay_trackers/cpp/replay_se3_tracker_impl.hpp b/src/core/replay_trackers/cpp/replay_se3_tracker_impl.hpp deleted file mode 100644 index a90a54bd5..000000000 --- a/src/core/replay_trackers/cpp/replay_se3_tracker_impl.hpp +++ /dev/null @@ -1,43 +0,0 @@ -// 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 - -namespace core -{ - -using Se3TrackerMcapViewers = McapTrackerViewers; - -class ReplaySe3TrackerImpl : public ISe3TrackerImpl -{ -public: - ReplaySe3TrackerImpl(std::unique_ptr reader, std::string_view base_name); - - ReplaySe3TrackerImpl(const ReplaySe3TrackerImpl&) = delete; - ReplaySe3TrackerImpl& operator=(const ReplaySe3TrackerImpl&) = delete; - ReplaySe3TrackerImpl(ReplaySe3TrackerImpl&&) = delete; - ReplaySe3TrackerImpl& operator=(ReplaySe3TrackerImpl&&) = delete; - - void update(int64_t monotonic_time_ns) override; - const Se3TrackerPoseTrackedT& get_data() const override; - -private: - Se3TrackerPoseTrackedT tracked_; - std::unique_ptr mcap_viewers_; - // Pre-baked warn-once message including the base_name, so multi-tracker replays are - // distinguishable in the log. - std::string no_data_message_; - // Warn only on the first frame of a no-data gap (EOF / sparse stream) to avoid per-frame spam. - bool warned_no_data_ = false; -}; - -} // namespace core diff --git a/src/core/retargeting_engine_tests/python/test_haptic_devices.py b/src/core/retargeting_engine_tests/python/test_haptic_devices.py index dce1a946d..b008a34ee 100644 --- a/src/core/retargeting_engine_tests/python/test_haptic_devices.py +++ b/src/core/retargeting_engine_tests/python/test_haptic_devices.py @@ -168,33 +168,32 @@ def test_flush_logs_failure_at_most_once_per_endpoint(self, caplog) -> None: ) -class _RecordingTensorPushTracker: - """Test double for ``TensorPushTracker``. +class _RecordingHapticPushTracker: + """Test double for ``HapticCommandPushTracker``. - Records ``push(session, payload)`` calls; ``fail=True`` makes every push + Records ``push(session, command)`` calls; ``fail=True`` makes every push raise so we can exercise ``PushTensorHapticDevice``'s once-per-endpoint - error gate. The endpoint is encoded inside ``payload`` (a HapticCommand - FlatBuffer), so tests assert on the raw bytes. + error gate. """ def __init__(self, fail: bool = False) -> None: - self.pushes: List[Tuple[object, bytes]] = [] + self.pushes: List[Tuple[object, object]] = [] self._fail = fail def get_name(self) -> str: - return "TensorPushTracker" + return "HapticCommandPushTracker" - def push(self, session, payload) -> None: + def push(self, session, command) -> None: if self._fail: raise RuntimeError("simulated push failure") - self.pushes.append((session, bytes(payload))) + self.pushes.append((session, command)) class TestPushTensorHapticDevice: - """Cross-process device: ``apply`` stores, ``flush`` encodes one - ``HapticCommand`` per endpoint and pushes it through a ``TensorPushTracker``. - The real tracker is swapped for a recording double so ``flush`` can run - without a live DeviceIO session.""" + """Cross-process device: ``apply`` stores, ``flush`` builds one + ``HapticCommand`` per endpoint and pushes it through a + ``HapticCommandPushTracker``. The real tracker is swapped for a recording + double so ``flush`` can run without a live DeviceIO session.""" def _device(self, **kwargs): from isaacteleop.haptic_devices.push_tensor import PushTensorHapticDevice @@ -218,7 +217,7 @@ def test_glove_factory_builds_finger_power_device(self) -> None: def test_apply_then_flush_pushes_encoded_command_per_endpoint(self) -> None: device = self._device() - recorder = _RecordingTensorPushTracker() + recorder = _RecordingHapticPushTracker() device._tracker = recorder # swap in the double; flush() needs no session device.apply("left", np.array([0.1, 0.2, 0.3, 0.4, 0.5], dtype=np.float32)) @@ -228,17 +227,14 @@ def test_apply_then_flush_pushes_encoded_command_per_endpoint(self) -> None: device.flush(sentinel_session) assert len(recorder.pushes) == 2 - for session, payload in recorder.pushes: + for session, command in recorder.pushes: assert session is sentinel_session - assert isinstance(payload, bytes) and len(payload) > 0 - # The endpoint name travels inside the serialized HapticCommand. - all_bytes = b"".join(payload for _s, payload in recorder.pushes) - assert b"left" in all_bytes - assert b"right" in all_bytes + assert command.endpoint in ("left", "right") + assert len(command.values) == 5 def test_apply_coalesces_to_latest_per_endpoint(self) -> None: device = self._device() - recorder = _RecordingTensorPushTracker() + recorder = _RecordingHapticPushTracker() device._tracker = recorder device.apply("left", np.full(5, 0.1, dtype=np.float32)) @@ -249,7 +245,7 @@ def test_apply_coalesces_to_latest_per_endpoint(self) -> None: def test_flush_clears_pending(self) -> None: device = self._device() - recorder = _RecordingTensorPushTracker() + recorder = _RecordingHapticPushTracker() device._tracker = recorder device.apply("left", np.zeros(5, dtype=np.float32)) @@ -260,7 +256,7 @@ def test_flush_clears_pending(self) -> None: def test_flush_swallows_exceptions_and_logs_once_per_endpoint(self, caplog) -> None: device = self._device(endpoints=("left",)) - device._tracker = _RecordingTensorPushTracker(fail=True) + device._tracker = _RecordingHapticPushTracker(fail=True) for _ in range(3): device.apply("left", np.zeros(5, dtype=np.float32)) diff --git a/src/python/isaacteleop/deviceio_trackers/__init__.py b/src/python/isaacteleop/deviceio_trackers/__init__.py index 2cdafa3e1..7913d5875 100644 --- a/src/python/isaacteleop/deviceio_trackers/__init__.py +++ b/src/python/isaacteleop/deviceio_trackers/__init__.py @@ -13,11 +13,8 @@ MessageChannelStatus, MessageChannelTracker, FrameMetadataTrackerOak, - Generic3AxisPedalTracker, - OgloTactileTracker, + HapticCommandReaderTracker, TensorPushTracker, - JointStateTracker, - Se3Tracker, FullBodyTracker, ITrackerSession, NUM_JOINTS, @@ -27,6 +24,11 @@ JOINT_INDEX_TIP, ) +# Tracker classes declared in trackers.toml. Star-imported (and appended to __all__ below) +# so a new manifest entry needs no edit here. +from ._generated_tracker_exports import * # noqa: F403 +from ._generated_tracker_exports import __all__ as _GENERATED_TRACKERS + # Deprecated aliases resolved lazily via __getattr__ so that accessing them emits a # DeprecationWarning. Intentionally omitted from __all__ so `import *` no longer pulls # the old names. @@ -51,10 +53,8 @@ def __getattr__(name: str): "MessageChannelTracker", "FrameMetadataTrackerOak", "FullBodyTracker", - "Generic3AxisPedalTracker", - "OgloTactileTracker", + "HapticCommandReaderTracker", "TensorPushTracker", - "JointStateTracker", "HandTracker", "HeadTracker", "ITracker", @@ -63,6 +63,6 @@ def __getattr__(name: str): "JOINT_THUMB_TIP", "JOINT_WRIST", "NUM_JOINTS", - "Se3Tracker", "ITrackerSession", + *_GENERATED_TRACKERS, ] diff --git a/src/python/isaacteleop/haptic_devices/push_tensor.py b/src/python/isaacteleop/haptic_devices/push_tensor.py index 73f262d5e..63473d7e1 100644 --- a/src/python/isaacteleop/haptic_devices/push_tensor.py +++ b/src/python/isaacteleop/haptic_devices/push_tensor.py @@ -29,9 +29,9 @@ import numpy as np -from isaacteleop.deviceio_trackers import TensorPushTracker +from isaacteleop.deviceio_trackers import HapticCommandPushTracker from isaacteleop.retargeting_engine.interface.tensor_group_type import TensorGroupType -from isaacteleop.schema import pack_haptic_command +from isaacteleop.schema import HapticCommand from .interface import Endpoint, IHapticDevice @@ -47,10 +47,9 @@ class PushTensorHapticDevice(IHapticDevice): """:class:`IHapticDevice` that pushes ``HapticCommand`` to a peer process. Each frame, :meth:`apply` stores the latest values per endpoint, and - :meth:`flush` serialises one ``HapticCommand`` per endpoint (via - ``pack_haptic_command``) and pushes it through a :class:`TensorPushTracker`. - The paired consumer process reads them back with a - ``HapticCommandReaderTracker`` on the same ``collection_id`` and + :meth:`flush` builds one ``HapticCommand`` per endpoint and pushes it through + a :class:`HapticCommandPushTracker`. The paired consumer process reads them + back with a ``HapticCommandReaderTracker`` on the same ``collection_id`` and ``tensor_identifier`` and drives the real hardware there. Args: @@ -77,11 +76,11 @@ def __init__( *, tensor_identifier: str = "haptic_command", endpoints: Iterable[Endpoint] = ("left", "right"), - max_payload_size: int = TensorPushTracker.DEFAULT_MAX_PAYLOAD_SIZE, + max_payload_size: int = HapticCommandPushTracker.DEFAULT_MAX_PAYLOAD_SIZE, ) -> None: self._accepted_type = accepted_type self._endpoints: tuple[Endpoint, ...] = tuple(endpoints) - self._tracker = TensorPushTracker( + self._tracker = HapticCommandPushTracker( collection_id, tensor_identifier, max_payload_size ) # Latest-wins per endpoint within a frame; emitted and cleared by flush. @@ -106,8 +105,9 @@ def flush(self, deviceio_session: Any) -> None: pending, self._pending = self._pending, {} for endpoint, values in pending.items(): try: - payload = pack_haptic_command(endpoint, values) - self._tracker.push(deviceio_session, payload) + self._tracker.push( + deviceio_session, HapticCommand(endpoint=endpoint, values=values) + ) except Exception as exc: if not self._error_logged.get(endpoint, False): logger.warning(