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:
Component |
Code Location |
|---|---|
Data Schema |
|
Device Plugin |
|
Tracker facade ( |
src/core/deviceio_trackers/cpp/generic_3axis_pedal_tracker.cpp and src/core/deviceio_trackers/cpp/inc/deviceio_trackers/generic_3axis_pedal_tracker.hpp |
Live backend ( |
src/core/live_trackers/cpp/live_generic_3axis_pedal_tracker_impl.cpp and src/core/live_trackers/cpp/live_generic_3axis_pedal_tracker_impl.hpp |
|
src/core/live_trackers/cpp/inc/live_trackers/schema_tracker.hpp |
Debug printer |
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
datafield (e.g.Generic3AxisPedalOutputTracked). Used by the in-memory tracker API so thatdatacan 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 inserialize_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
OpenXRSessionwith extensions fromSchemaPusher::get_required_extensions()(includesXR_NVX1_push_tensorandXR_NVX1_tensor_data).SchemaPusher — Construct a
SchemaPusherwithOpenXRSessionHandlesand aSchemaPusherConfig:collection_id,max_flatbuffer_size,tensor_identifier,localized_name, and optionallyapp_name. Thecollection_idandtensor_identifiermust match what the tracker uses.Push loop — In your update loop, fill the schema’s native type (e.g.
Generic3AxisPedalOutputT), serialize it withFlatBufferBuilderand the generatedPack(), then callpusher_.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(), andget_record_channels(). For OpenXR, add a staticrequired_extensions()on your liveITrackerImpland register the tracker type inLiveDeviceIOFactory::get_required_extensions(tensor/schema readers usually forwardSchemaTrackerBase::get_required_extensions()). Callers useDeviceIOSession::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 liveITrackerImplbuilds 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 anITrackerImplthat holds aSchemaTracker, builds aSchemaTrackerConfigfrom the tracker’s stored configuration, and implementsupdate(XrTime)andserialize_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. setm_tracked.data = nullptr). Otherwise, deserialize the latest sample (or all samples) into your tracked type and keep the last one forget_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:
Tracker facade — src/core/deviceio_trackers/cpp/generic_3axis_pedal_tracker.cpp (class
Generic3AxisPedalTracker): holds collection configuration, implementsITracker, and exposesget_data(session)returningGeneric3AxisPedalOutputTrackedTby dispatching to the session’sIGeneric3AxisPedalTrackerImpl(see src/core/deviceio_base/cpp/inc/deviceio_base/generic_3axis_pedal_tracker_base.hpp).Live backend — src/core/live_trackers/cpp/live_generic_3axis_pedal_tracker_impl.cpp (
LiveGeneric3AxisPedalTrackerImpl): composesSchemaTracker, implementsupdate()andserialize_all(), and usesSchemaTracker::read_all_samples()withstd::vector<SchemaTracker::SampleResult>for the pending batch. See src/core/live_trackers/cpp/inc/live_trackers/schema_tracker.hpp forSchemaTrackerandSampleResult(buffer + timestamp metadata).
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):
Create the tracker (e.g.
std::make_shared<Generic3AxisPedalTracker>(collection_id, max_flatbuffer_size)).Get required extensions with
DeviceIOSession::get_required_extensions(trackers)and create anOpenXRSession.Create a
DeviceIOSessionwithDeviceIOSession::run(trackers, oxr_session->get_handles()).Loop: call
session->update(), then readtracker->get_data(*session). Iftracked.datais 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 (
pusheriolibrary) — Pushes serialized FlatBuffer data via OpenXR tensor extensions: takes externally-provided OpenXR session handles, creates a tensor collection with the configured identifier, providespush_buffer()for raw serialized data. Use composition to create typed wrappers (e.g.Generic3AxisPedalPusherin examples/schemaio/pedal_pusher.cpp).SchemaTracker (
live_trackers) — Helper for reading FlatBuffer schema data via OpenXR tensor collections: discovers collections by identifier, exposesread_all_samples()intoSampleResultvalues. Live tracker implementations (e.g.LiveGeneric3AxisPedalTrackerImpl) compose aSchemaTrackerand implementITrackerImpl::update()/serialize_all().Generic3AxisPedalTracker (tracker facade in
deviceio_trackers) — ConcreteITrackerforGeneric3AxisPedalOutput: holds configuration andget_data(session)returningGeneric3AxisPedalOutputTrackedTvia the session’sIGeneric3AxisPedalTrackerImpl.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.