Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/source/device/add_device.rst
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,9 @@ Use ``direction = "push"`` when Teleop **pushes** a typed table to a plugin (e.g
``HapticCommand.endpoint`` samples on one collection (e.g. ``HapticCommandReaderTracker`` for Manus
haptics) stay hand-written — see :doc:`../references/generated_trackers`.

A device that describes itself once (a layout, a calibration) rather than in every frame declares
that description as the tracker's ``metadata``; see :ref:`tracker-metadata`.

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

Expand Down
35 changes: 35 additions & 0 deletions docs/source/references/generated_trackers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,41 @@ Python edit at all.
``.pyi`` stubs need no work either: :code-file:`src/core/python/generate_stubs.py` derives them from
the built module.

.. _tracker-metadata:

Trackers with metadata
----------------------

A device that describes itself once rather than every frame (a tactile glove's taxel layout, for
example) declares that description as the tracker's ``metadata`` table:

.. code-block:: toml

[[tracker]]
name = "tactile"
table = "TactileData"
metadata = "TactileShape"

The metadata table is the ``root_type`` of a schema of its own, named after the table in snake case
(``tactile_shape.fbs``), with no Record wrapper: MCAP records the table itself. Like the data table,
it needs a hand-written ``schema/python/*_bindings.h`` before ``get_metadata()`` can return it to
Python. Only ``direction = "pull"`` supports ``metadata``.

- **Producer.** Pushes the encoded table on ``tracker_metadata_collection_id(collection_id)``
(``<collection_id>/metadata``), tensor ``TRACKER_METADATA_TENSOR_IDENTIFIER``, in samples of at most
``TRACKER_METADATA_MAX_SIZE`` bytes; see :code-file:`src/core/schema/cpp/inc/schema/tracker_metadata.hpp`.
It pushes when it starts and periodically after that, always the same encoded table, so a consumer
that starts later still receives it. A C++ producer does this with a ``SchemaPusher``, a Python one
with a ``TensorPushTracker``, either sized to the encoded table.
- **Live.** The first metadata received is adopted, recorded once on ``<channel>_metadata``, and
returned by ``get_metadata()`` for the rest of the session. ``get_data()`` publishes and records
data only while the producer's metadata is confirmed: from a sample equal to the adopted one
until the metadata collection goes away, so a restarted producer's data waits for its own
metadata. A sample that differs from the adopted one, or is not a valid table, is an error the
session update throws.
- **Replay.** Reads the recorded metadata when the session opens, and publishes data only from a
recording that holds it.

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

Expand Down
11 changes: 11 additions & 0 deletions src/core/codegen/manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,17 @@ def resolve_tracker_entry(
f"tracker '{raw['name']}': record must be a boolean, got {type(record).__name__}"
)

metadata = resolved.get("metadata")
if metadata is not None:
if not isinstance(metadata, str) or not metadata:
raise ValueError(
f"tracker '{raw['name']}': metadata must name a table, got {metadata!r}"
)
if resolved["direction"] != "pull":
raise ValueError(
f"tracker '{raw['name']}': metadata requires direction=pull"
)

if resolved["direction"] == "pull" and record:
for key in ("mcap_channels", "replay_channels"):
if key not in resolved:
Expand Down
5 changes: 5 additions & 0 deletions src/core/codegen/template_renderer.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ def template_values(ctx: TrackerGenContext) -> dict[str, str]:
"PYTHON_ACCESSOR": ctx.python_accessor,
"TENSOR_IDENTIFIER_EXPR": _tensor_identifier_expr(ctx),
"HAS_TENSOR_IDENTIFIER": "1" if ctx.facade_tensor_constant else "0",
"HAS_METADATA": "1" if ctx.metadata else "0",
# Only referenced inside `#if @HAS_METADATA@` blocks.
"METADATA": ctx.metadata.table if ctx.metadata else "",
"METADATA_SCHEMA": ctx.metadata.schema if ctx.metadata else "",
"METADATA_CHANNEL": ctx.metadata.channel if ctx.metadata else "",
}
if ctx.mcap_channels is not None:
values["RECORDING_CHANNELS"] = ", ".join(f'"{c}"' for c in ctx.mcap_channels)
Expand Down
30 changes: 30 additions & 0 deletions src/core/codegen/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from __future__ import annotations

import re
from dataclasses import dataclass
from typing import Any

Expand All @@ -19,6 +20,22 @@ def _header() -> str:
)


def camel_to_snake(value: str) -> str:
"""Convert CamelCase to snake_case (TactileShape -> tactile_shape)."""
return re.sub(
r"(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])", "_", value
).lower()


@dataclass(frozen=True, slots=True)
class MetadataGenContext:
"""The ``metadata`` table, the ``.fbs`` it is the root of, and its MCAP sub-channel."""

table: str
schema: str
channel: str


@dataclass(frozen=True, slots=True)
class TrackerGenContext:
"""Resolved manifest entry plus derived C++/file names for codegen."""
Expand All @@ -39,6 +56,7 @@ class TrackerGenContext:
facade_tensor_constant: bool
mcap_channels: tuple[str, ...] | None
replay_channels: tuple[str, ...] | None
metadata: MetadataGenContext | None
header: str
base_header: str
live_impl_file: str
Expand Down Expand Up @@ -82,6 +100,17 @@ def enrich_context(entry: dict[str, Any]) -> TrackerGenContext:
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

metadata_table = entry.get("metadata")
metadata = (
MetadataGenContext(
table=metadata_table,
schema=camel_to_snake(metadata_table),
channel=f"{entry['channel']}_metadata",
)
if metadata_table is not None
else None
)

return TrackerGenContext(
name=name,
table=table,
Expand All @@ -99,6 +128,7 @@ def enrich_context(entry: dict[str, Any]) -> TrackerGenContext:
facade_tensor_constant=bool(entry["facade_tensor_constant"]),
mcap_channels=mcap_channels,
replay_channels=replay_channels,
metadata=metadata,
header=header,
base_header=base,
live_impl_file=live_impl_file,
Expand Down
11 changes: 10 additions & 1 deletion src/core/codegen/templates/fragments/pybind_pull.template
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,13 @@
[](const core::@CLASS@& self, const core::ITrackerSession& session)
{ return to_python(self.get_data(session)); },
py::arg("session"),
"Get the current state (None when no data available)");
"Get the current state (None when no data available)")
#if @HAS_METADATA@
.def(
"get_metadata",
[](const core::@CLASS@& self, const core::ITrackerSession& session)
{ return to_python(self.get_metadata(session)); },
Comment thread
lotusl-code marked this conversation as resolved.
py::arg("session"),
"Get the first metadata received from the producer (None until it arrives, then fixed for the session)")
#endif
;
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@ struct @TRAITS@
{
static constexpr std::array recording_channels = { @RECORDING_CHANNELS@ };
static constexpr std::array replay_channels = { @REPLAY_CHANNELS@ };
#if @HAS_METADATA@
static constexpr std::array metadata_channels = { "@METADATA_CHANNEL@" };
#endif
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
std::unique_ptr<@IFACE@> ReplayDeviceIOFactory::create_@NAME@_tracker_impl(const @CLASS@* tracker)
{
#if @HAS_METADATA@
// Each McapTrackerViewers iterates a reader of its own, so the metadata is read through a second one.
auto metadata = @REPLAY_IMPL@::read_metadata(open_reader(filename_), get_name(tracker), recorded_schemas_);
return std::make_unique<@REPLAY_IMPL@>(
open_reader(filename_), get_name(tracker), recorded_schemas_, std::move(metadata));
#else
return std::make_unique<@REPLAY_IMPL@>(open_reader(filename_), get_name(tracker), recorded_schemas_);
#endif
}
6 changes: 6 additions & 0 deletions src/core/codegen/templates/pull/deviceio_base.hpp.template
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,15 @@
namespace core
{
struct @FB_TABLE@;
#if @HAS_METADATA@
struct @METADATA@;
#endif
class @IFACE@ : public ITrackerImpl
{
public:
virtual const Serialized<@FB_TABLE@>& get_data() const = 0;
#if @HAS_METADATA@
virtual const Serialized<@METADATA@>& get_metadata() const = 0;
#endif
};
} // namespace core
6 changes: 6 additions & 0 deletions src/core/codegen/templates/pull/facade.cpp.template
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,10 @@ const Serialized<@FB_TABLE@>& @CLASS@::get_data(const ITrackerSession& session)
{
return static_cast<const @IFACE@&>(session.get_tracker_impl(*this)).get_data();
}
#if @HAS_METADATA@
const Serialized<@METADATA@>& @CLASS@::get_metadata(const ITrackerSession& session) const
{
return static_cast<const @IFACE@&>(session.get_tracker_impl(*this)).get_metadata();
}
#endif
} // namespace core
9 changes: 9 additions & 0 deletions src/core/codegen/templates/pull/facade.hpp.template
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
#pragma once
#include <deviceio_base/@BASE_HEADER@.hpp>
#include <schema/@SCHEMA@_generated.h>
#if @HAS_METADATA@
#include <schema/@METADATA_SCHEMA@_generated.h>
#endif

#include <cstddef>
#include <string>
Expand All @@ -25,6 +28,12 @@ public:
return TRACKER_NAME;
}
const Serialized<@FB_TABLE@>& get_data(const ITrackerSession& session) const;
#if @HAS_METADATA@
//! The first metadata received from the producer: empty until it arrives, then fixed for the session.
//! get_data() is empty until the producer's metadata is confirmed. A different one from the
//! producer makes the session update throw.
const Serialized<@METADATA@>& get_metadata(const ITrackerSession& session) const;
#endif
const std::string& collection_id() const
{
return collection_id_;
Expand Down
36 changes: 36 additions & 0 deletions src/core/codegen/templates/pull/live.cpp.template
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
#include "@LIVE_IMPL_FILE@.hpp"

#include <mcap/recording_traits.hpp>
#if @HAS_METADATA@
#include <schema/@METADATA_SCHEMA@_bfbs_generated.h>
#endif
#include <schema/@SCHEMA@_bfbs_generated.h>

namespace core
Expand All @@ -20,6 +23,18 @@ SchemaTrackerConfig make_@NAME@_tensor_config(const @CLASS@* tracker)
return cfg;
}
} // namespace
#if @HAS_METADATA@
@MCAP_CHANNELS_TYPE@::@MCAP_CHANNELS_TYPE@(mcap::McapWriter& writer,
std::string_view base_name,
const std::vector<std::string>& sub_channels)
: data(writer, base_name, sub_channels),
metadata(writer,
base_name,
std::vector<std::string>(@TRAITS@::metadata_channels.begin(), @TRAITS@::metadata_channels.end()))
{
// No-op: members set in the initializer list.
}
#endif
std::unique_ptr<@MCAP_CHANNELS_TYPE@> @LIVE_IMPL@::create_mcap_channels(mcap::McapWriter& writer,
std::string_view base_name)
{
Expand All @@ -31,17 +46,38 @@ std::unique_ptr<@MCAP_CHANNELS_TYPE@> @LIVE_IMPL@::create_mcap_channels(mcap::Mc
const @CLASS@* tracker,
std::unique_ptr<@MCAP_CHANNELS_TYPE@> mcap_channels)
: mcap_channels_(std::move(mcap_channels)),
#if @HAS_METADATA@
m_schema_reader(handles, make_@NAME@_tensor_config(tracker), mcap_channels_ ? &mcap_channels_->data : nullptr, 0, 1),
m_metadata_reader(handles, tracker->collection_id(), mcap_channels_ ? &mcap_channels_->metadata : nullptr)
#else
m_schema_reader(handles, make_@NAME@_tensor_config(tracker), mcap_channels_.get(), 0, 1)
#endif
{
// No-op: members set in the initializer list.
}
void @LIVE_IMPL@::update(int64_t /*monotonic_time_ns*/)
{
#if @HAS_METADATA@
// Metadata first, so it is recorded ahead of the data it lets through on the same tick.
const bool publish = m_metadata_reader.update();
m_schema_reader.update(m_tracked, publish);
if (!publish)
{
m_tracked.reset();
}
#else
m_schema_reader.update(m_tracked);
#endif
}
const Serialized<@FB_TABLE@>& @LIVE_IMPL@::get_data() const
{
return m_tracked;
}
#if @HAS_METADATA@
const Serialized<@METADATA@>& @LIVE_IMPL@::get_metadata() const
{
return m_metadata_reader.get();
}
#endif

} // namespace core
23 changes: 23 additions & 0 deletions src/core/codegen/templates/pull/live.hpp.template
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@

#pragma once
#include <deviceio_trackers/@HEADER@.hpp>
#if @HAS_METADATA@
#include <live_trackers/schema_metadata_tracker.hpp>
#endif
#include <live_trackers/schema_tracker.hpp>
#include <oxr_utils/oxr_session_handles.hpp>
#if @HAS_METADATA@
#include <schema/@METADATA_SCHEMA@_generated.h>
#endif
#include <schema/@SCHEMA@_generated.h>

#include <cstdint>
Expand All @@ -13,7 +19,18 @@
#include <vector>
namespace core
{
#if @HAS_METADATA@
// The data and metadata channels, created together so the factory hands over one object.
struct @MCAP_CHANNELS_TYPE@
{
@MCAP_CHANNELS_TYPE@(mcap::McapWriter& writer, std::string_view base_name, const std::vector<std::string>& sub_channels);

McapTrackerChannels<@RECORD_TYPE@> data;
McapTrackerChannels<@METADATA@> metadata;
};
#else
using @MCAP_CHANNELS_TYPE@ = McapTrackerChannels<@RECORD_TYPE@>;
#endif
using @SCHEMA_TRACKER_TYPE@ = SchemaTracker<@RECORD_TYPE@, @FB_TABLE@>;
class @LIVE_IMPL@ : public @IFACE@
{
Expand All @@ -29,10 +46,16 @@ public:
std::unique_ptr<@MCAP_CHANNELS_TYPE@> mcap_channels);
void update(int64_t monotonic_time_ns) override;
const Serialized<@FB_TABLE@>& get_data() const override;
#if @HAS_METADATA@
const Serialized<@METADATA@>& get_metadata() const override;
#endif

private:
std::unique_ptr<@MCAP_CHANNELS_TYPE@> mcap_channels_;
@SCHEMA_TRACKER_TYPE@ m_schema_reader;
#if @HAS_METADATA@
SchemaMetadataTracker<@METADATA@> m_metadata_reader;
#endif
Serialized<@FB_TABLE@> m_tracked;
};
} // namespace core
Loading
Loading