hydrate-protobuf-v2 (Transform Node)
The Hydrate Protobuf v2 node decodes HEAT capture data shaped as DataPacket / DataPacketBundle (see HEAT.Proto.Capture in the protolib), walks every nested packet, and extracts a single kind of simulation payload from each SimulationFrame: the full frame, each EntityState, each EntityDataEvent (from frame.events where the EntityEvent carries data_event), or each TrackingEvent (from tracking_event).
Use this node when you need capture-aware extraction with stable envelope fields (parent input key, packet timestamps, origin_uuid, simulation tick/time) across all upstream parents. Optional projection filters prune components and identity so domain trees avoid multi‑hundred‑MB overhydration. The original hydrate-protobuf node remains for Gaze, FolderUploadData, OpenDIS PDU, and its legacy SimulationFrame component flattening.
Capture hierarchy
Configuration schema
| Property | Type | Required | Description |
|---|---|---|---|
extractionTarget | string | Yes | One of SimulationFrame, EntityState, EntityDataEvent, TrackingEvent. |
outputFormat | "json" | "csv" | Yes | json: UTF-8 JSON object with a top-level records array (or NDJSON when lineDelimitedJson is true). csv: scalar columns plus record_json (full row JSON) per line. |
sortBy | string | No | none (default), simulation_time_seconds, or packet_timestamp_unix_ms for stable ordering after merging parents. |
dataSourceName | string | No | Output data source; defaults to the runner default_store when omitted. |
enableUploadDeduplication | boolean | No | Legacy flag. Streaming full replay and incremental paths always skip upload when the artefact md5 matches the prior output (live anti-baggage). Default false. |
lineDelimitedJson | boolean | No | When true with outputFormat: json, emit one JSON record object per line (NDJSON) instead of a records wrapper. Required when incrementalHydration is true unless cumulativeRecordsJson is set. Default false. |
incrementalHydration | boolean | No | Opt-in. Stream-append only new parent outputs to cumulative NDJSON; requires lineDelimitedJson (or cumulativeRecordsJson). Supported for EntityDataEvent and TrackingEvent. Default false (full replay). |
cumulativeRecordsJson | boolean | No | Opt-in. With incrementalHydration, publish the cumulative { "records": [...] } object (the shape Arbex openJsonReader needs) instead of NDJSON. The prior artefact is re-read as the base and only parent outputs above the watermark are decoded. Prior records are held in memory, so use it for filtered lanes (edges-only button presses), not raw telemetry. Cannot be combined with lineDelimitedJson. Default false. |
includeComponentTypes | string[] | No | Allow-list of Component oneof arm names (for example world_position, dynamics). Mutually exclusive with excludeComponentTypes. Applies to EntityState and SimulationFrame. |
excludeComponentTypes | string[] | No | Deny-list of component oneof arms. Mutually exclusive with includeComponentTypes. |
dropEmptyEntities | boolean | No | When component filters are set, omit entities with zero remaining components. Default true. |
filterOriginUuid | string | No | Exact match on capture origin_uuid. |
filterEntityId | integer | No | Exact match on entity id (EntityState entity.id or event entity_id). |
filterEntityName | string | No | Exact case-sensitive match on EntityState entity.name. |
entityNameRegex | string | No | Python regex fullmatch on EntityState entity.name. AND-combined with other identity filters. |
includeDataEventTypes | string[] | No | Allow-list of EntityDataEvent event_data oneof arms (for example button_press). |
includeJsonEventNames | string[] | No | Allow-list of json_event event_name values (for example start_vignette, stop_vignette). Applied after includeDataEventTypes; unset keeps every json_event. Use it so a marker lane does not carry per-tick JSON such as ControllerAxes. |
inlineMediaContentPrefixes | string[] | No | MIME prefixes (for example audio/) whose event_data.content is truncated to its header, dropping the inline payload. Unset or empty keeps the value whole, which is the default. See Inline media content. |
buttonPressEdgesOnly | boolean | No | When true, keep only button_press edges: emit on first sample, on event_data.state change, or after a silence longer than buttonPressEdgeGapMs. Drops held/polled ticks (common for joystick or 2D axis readouts reported as buttons). Default false. |
buttonPressEdgeGapMs | integer | No | Gap in milliseconds that ends a hold when buttonPressEdgesOnly is true. Default 100. |
includeTrackingEventTypes | string[] | No | Allow-list of TrackingEventType enum names (for example TRACKING_EVENT_TYPE_FIRED). Applies to TrackingEvent and to EntityDataEvent.event_type when present. |
When no projection filters are set, behavior matches the previous full-tree extraction.
Incremental hydration (opt-in)
Incremental hydration still re-uploads the whole cumulative NDJSON on every run where the bytes changed. For live Capture lanes use hydrate-protobuf-journal with extractionTarget and journalFormat: "ndjson", which publishes only the new records as an append-only part.
Same contract as hydrate-protobuf: cumulative NDJSON per run, watermark on output config, downstream inputMode: latest. Supported for EntityDataEvent and TrackingEvent extraction.
Live anti-baggage: when the newly built artefact has the same md5 as the node’s current output, the processor completes without uploading. That matches OpenDIS CSV incremental behaviour and avoids creating a new NodeOutput (and child fan-out) on every agnostic-input tick when nothing in this lane changed. The watermark stays on the prior output until content actually changes.
For button_press streams, combine buttonPressEdgesOnly with incrementalHydration so the cumulative artefact stays small and only grows when real edges appear.
Records-JSON variant (cumulativeRecordsJson: true): same watermark and md5-skip contract, but the artefact stays a records object. With buttonPressEdgesOnly, the edge filter state (last held sample per origin, entity and button) is persisted as buttonPressEdgeState on the output configuration and seeded on the next run, so a hold that straddles two triggers is not re-emitted as a new press. On an md5 skip the watermark and edge state are patched onto the prior output. Samples older than the seeded state (retroactive uploads) fall back to state-change detection only; a full replay remains the order-exact path.
Output shape (JSON)
The artefact is a JSON object:
{
"extractionTarget": "EntityState",
"records": [
{
"parent_input_key": "upstream-node-name",
"payload_index": 0,
"inner_packet_index": 2,
"bundle_timestamp_unix_ms": null,
"packet_timestamp_unix_ms": 1710000000000,
"packet_timestamp_iso_utc": "2024-03-09T12:00:00.000Z",
"origin_uuid": "…",
"simulation_tick": 42,
"simulation_time_seconds": 1.5,
"capture_time_timestamp_ms": null,
"capture_time_iso_utc": null,
"capture_time_timezone_offset_minutes": 0,
"extraction_target": "EntityState",
"entity_state_index": 0,
"entity_state": { }
}
]
}For EntityDataEvent / TrackingEvent, each record follows the decoder event dictionary layout (event_category, event_type, event_data, …) plus the same envelope fields merged in.
Example configurations
Full EntityState (legacy behavior):
{
"extractionTarget": "EntityState",
"outputFormat": "json",
"sortBy": "simulation_time_seconds",
"dataSourceName": "my-store"
}Domain tree (world position only, named entities):
{
"extractionTarget": "EntityState",
"outputFormat": "json",
"includeComponentTypes": ["world_position", "dynamics"],
"entityNameRegex": "^(Blue|Red)-.*",
"dropEmptyEntities": true
}Tracking timeline for one entity:
{
"extractionTarget": "TrackingEvent",
"outputFormat": "json",
"lineDelimitedJson": true,
"incrementalHydration": true,
"filterEntityId": 42,
"includeTrackingEventTypes": ["TRACKING_EVENT_TYPE_FIRED", "TRACKING_EVENT_TYPE_HIT"]
}Inputs and outputs
- Inputs: All parents are merged. Each parent contributes a list of binary
DataPacketpayloads (same wire format as v1). - Output: One JSON or CSV artefact. Unlike v1, every parent key in
fetch_all_inputsis processed, not only the first.
Inline media content
Some capture emitters put a media stream on a string field instead of emitting it
on its own channel. A radio feed can arrive on CommsEvent.content as:
audio/pcm;src=mic;rate=48000;bits=32;ch=2;enc=base64,<megabytes of base64>Every artefact, merge and dashboard script downstream of the hydrate node then
carries those bytes, usually so that the single consumer which reads the field can
discard them. With incrementalHydration and cumulativeRecordsJson the payload
is also held in memory for the whole run.
inlineMediaContentPrefixes truncates such a value at the first comma, keeping the
header and dropping the payload:
{ "inlineMediaContentPrefixes": ["audio/"] }The header is kept rather than blanking the field so that the record stays self-describing: a reader can still see the declared sample rate and bit depth, and a MIME-prefix test still matches, at a few tens of bytes instead of the payload. An empty string would also make “this was media, handled elsewhere” look identical to “this event genuinely had no content”.
Defaults, and why they should stay
Unset or empty is a no-op. Any node that says nothing about inline media behaves exactly as it did before this option existed.
Do not make truncation the default. It will look like an obvious improvement
later, because nobody wants megabytes of base64 in a JSON artefact. But a consumer
that legitimately wants the inline payload out of this node would then get an empty
header with no error and no warning, and hydrate-protobuf-v2 is a long-lived
contract whose changes are additive by default.
It is lossy here, not lost
The raw capture blob keeps the full payload, so nothing is destroyed. A consumer
that needs the bytes reads the blob directly, as hydrate-protobuf does when
decoding CommsEvent audio (see hydrate-protobuf),
or the node is reprocessed with no prefixes configured.
The truncation is applied on this node’s EntityDataEvent extraction only, never in
the shared protobuf decoder, precisely because hydrate-protobuf reads the same
field to recover that audio.
Related
- Core Utils Runner
- Hydrate Protobuf (v1)
- system-protobuf-extract (platform subset with the same projection knobs)
- Node Templates