Add a New Device#

For new hardware that requires a custom driver or SDK, create an Isaac Teleop device plugin (C++ level). Plugins push data via OpenXR tensor collections. Existing plugins include Manus gloves, OAK-D camera, Haptikos exoskeletons, controller synthetic hands, and foot pedals. After creating the plugin, update the retargeting pipeline config to consume data from the new plugin’s source node. See the Plugins directory for examples.

To add a new device that streams typed data over the OpenXR runtime, follow these four steps. The reference implementation is the generic 3-axis foot pedal:

Step 1: Define the data schema#

Define a FlatBuffer schema (.fbs) under src/core/schema/fbs. The schema drives both serialization in the plugin and deserialization in the tracker; pusher and reader must agree on the same schema ahead of time (the schema is not sent over the wire).

Reference schema: src/core/schema/fbs/pedals.fbs

include "point.fbs";
include "timestamp.fbs";

namespace core;

table Generic3AxisPedalOutput {
  left_pedal: float (id: 0);
  right_pedal: float (id: 1);
  rudder: float (id: 2);
}

table Generic3AxisPedalOutputRecord {
  data: Generic3AxisPedalOutput (id: 0);
  timestamp: DeviceDataTimestamp (id: 1);
}
  • Output table — The primary payload type (e.g. Generic3AxisPedalOutput) with the device fields. This is what the plugin serializes and pushes.

  • Tracked wrapper — A table that wraps the output in an optional data field (e.g. Generic3AxisPedalOutputTracked). Used by the in-memory tracker API so that data can be null when no sample is available.

  • Record wrapper — A table that wraps the output plus DeviceDataTimestamp (e.g. Generic3AxisPedalOutputRecord). This is the root type written to MCAP channels by the recorder; trackers serialize into this type in serialize_all().

  • root_type — Set to the Record type (e.g. root_type Generic3AxisPedalOutputRecord;).

Include timestamp.fbs for DeviceDataTimestamp; include other shared types (e.g. point.fbs) as needed. After adding or changing a schema, rebuild so that the C++ and Python generated code (e.g. pedals_generated.h, pedals_bfbs_generated.h) is updated.

Step 2: Implement a device plugin#

The plugin runs in a separate process (or as part of a host app), reads hardware, and pushes serialized FlatBuffer data via OpenXR using the SchemaPusher from the pusherio library. Reuse the same pattern as the example apps in examples/schemaio/ (e.g. pedal_pusher which uses SchemaPusher).

  • OpenXR session — Create an OpenXRSession with extensions from SchemaPusher::get_required_extensions() (includes XR_NVX1_push_tensor and XR_NVX1_tensor_data).

  • SchemaPusher — Construct a SchemaPusher with OpenXRSessionHandles and a SchemaPusherConfig: collection_id, max_flatbuffer_size, tensor_identifier, localized_name, and optionally app_name. The collection_id and tensor_identifier must match what the tracker uses.

  • Push loop — In your update loop, fill the schema’s native type (e.g. Generic3AxisPedalOutputT), serialize it with FlatBufferBuilder and the generated Pack(), then call pusher_.push_buffer(ptr, size, sample_time_local_common_clock_ns, sample_time_raw_device_clock_ns). Use a monotonic clock (e.g. core::os_monotonic_now_ns()) for the local common clock; use the device’s own clock for the raw device clock if available.

Reference implementation: src/plugins/generic_3axis_pedal. The plugin holds a core::SchemaPusher member, opens the Linux joystick device, maps axes to Generic3AxisPedalOutputT, and calls push_current_state() from update(). See generic_3axis_pedal_plugin.hpp and generic_3axis_pedal_plugin.cpp.

Step 3: Implement a tracker#

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:

  • Extends ITracker — Override get_name(), get_schema_name(), get_schema_text(), and get_record_channels(). For OpenXR, add a static required_extensions() on your live ITrackerImpl and register the tracker type in LiveDeviceIOFactory::get_required_extensions (tensor/schema readers usually forward SchemaTrackerBase::get_required_extensions()). Callers use DeviceIOSession::get_required_extensions(trackers). Return the Record type name and binary schema for MCAP; return at least one channel name.

  • Holds user configuration — Same logical inputs as the pusher (e.g. collection_id, max_flatbuffer_size). The live ITrackerImpl builds the internal tensor settings (SchemaTrackerConfig) so they match the plugin.

  • Factory registration — Register your tracker in the live factory dispatch table (see LiveDeviceIOFactory). The factory constructs an ITrackerImpl that holds a SchemaTracker, builds a SchemaTrackerConfig from the tracker’s stored configuration, and implements update(XrTime) and serialize_all(channel_index, callback).

In the Impl:

  • update() — Call m_schema_reader.read_all_samples(pending_records). If the collection is not present, clear the tracked state (e.g. set m_tracked.data = nullptr). Otherwise, deserialize the latest sample (or all samples) into your tracked type and keep the last one for get_data().

  • serialize_all() — For each sample in the pending batch, deserialize, build the Record FlatBuffer (output table + DeviceDataTimestamp), and invoke the callback with (log_time_ns, buffer_ptr, size). The buffer is only valid during the callback. If the 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:

Step 4: Implement a simple C++ printer (optional)#

A minimal reader app verifies the full path: plugin (or pusher) pushes; printer discovers the collection and prints samples. Pattern (see examples/schemaio/pedal_printer.cpp):

  1. Create the tracker (e.g. std::make_shared<Generic3AxisPedalTracker>(collection_id, max_flatbuffer_size)).

  2. Get required extensions with DeviceIOSession::get_required_extensions(trackers) and create an OpenXRSession.

  3. Create a DeviceIOSession with DeviceIOSession::run(trackers, oxr_session->get_handles()).

  4. Loop: call session->update(), then read tracker->get_data(*session). If tracked.data is non-null, use the latest sample; otherwise sleep briefly and repeat.

Use the same collection_id (and optionally tensor_identifier) as the plugin. See Schema IO example: build and run above for building and running pedal_pusher and pedal_printer.

Schema IO example: build and run#

The Schema IO example is a simpler example that demonstrates pushing and reading serialized FlatBuffer data via the OpenXR runtime using the Generic Tensor Collection interface. It provides two binaries: pedal_pusher (serializes and pushes Generic3AxisPedalOutput using SchemaPusher) and pedal_printer (reads via Generic3AxisPedalTracker and DeviceIOSession). Both use the XR_NVX1_push_tensor and XR_NVX1_tensor_data extensions. Pusher and reader agree on the schema (Generic3AxisPedalOutput from pedals.fbs), so the schema is not sent over the wire.

Build (from the project root, with examples enabled):

cmake -B build -DBUILD_EXAMPLES=ON
cmake --build build --parallel
cmake --install build

Run pusher and printer in separate terminals:

# Terminal 1: Start printer
./install/examples/schemaio/pedal_printer

# Terminal 2: Start pusher
./install/examples/schemaio/pedal_pusher

The printer discovers the tensor collection created by the pusher and prints received samples. Both exit after 100 samples, or press Ctrl+C to exit early.

Components

  • SchemaPusher (pusherio library) — Pushes serialized FlatBuffer data via OpenXR tensor extensions: takes externally-provided OpenXR session handles, creates a tensor collection with the configured identifier, provides push_buffer() for raw serialized data. Use composition to create typed wrappers (e.g. Generic3AxisPedalPusher in examples/schemaio/pedal_pusher.cpp).

  • SchemaTracker (live_trackers) — Helper for reading FlatBuffer schema data via OpenXR tensor collections: discovers collections by identifier, exposes read_all_samples() into SampleResult values. Live tracker implementations (e.g. LiveGeneric3AxisPedalTrackerImpl) compose a SchemaTracker and implement ITrackerImpl::update() / serialize_all().

  • Generic3AxisPedalTracker (tracker facade in deviceio_trackers) — Concrete ITracker for Generic3AxisPedalOutput: holds configuration and get_data(session) returning Generic3AxisPedalOutputTrackedT via the session’s IGeneric3AxisPedalTrackerImpl.

  • DeviceIOSession — Session manager: collects required OpenXR extensions from registered trackers, creates tracker implementations with session handles, and calls update() on all trackers during the update loop.