Add a New Device#
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/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.