Scene Controller Data Formats#
Message Formats Overview#
Message Format |
Direction |
MQTT Topic |
|---|---|---|
Subscribe |
|
|
Subscribe |
|
|
Publish |
|
The Scene Controller only tracks objects and publishes unregulated per-category output. Sensor
correlation, regulated (rate-controlled) output, and region/tripwire event publishing are owned
by the Analytics microservice — see
Analytics Service Data Formats for those message formats
(scenescape/data/sensor/{sensor_id}, scenescape/regulated/scene/{scene_id},
scenescape/event/region/{scene_id}/{region_id}/{event_type}, scenescape/event/tripwire/{scene_id}/{tripwire_id}/{event_type}).
Camera Input Message Format#
The Scene Controller subscribes to the MQTT topic scenescape/data/camera/{camera_id} and
receives camera detection metadata from visual analytics pipelines. Messages are validated
against the detector definition in
metadata.schema.json.
Top-Level Message Fields#
Field |
Type |
Required |
Description |
|---|---|---|---|
|
string |
Yes |
Camera identifier; must match the |
|
string (ISO 8601 UTC) |
Yes |
Acquisition time of the frame |
|
object |
Yes |
Category-keyed map; each value is an array of detections (e.g. |
|
number ≥ 0 |
No |
Camera framerate (frames per second) when the message was produced |
|
array of string |
No |
Sub-detection labels run on this frame (e.g. |
|
object |
No |
Camera intrinsic parameters ( |
|
object |
No |
Lens distortion coefficients keyed by name ( |
Detection Object Fields (objects.<category>[*])#
Field |
Type |
Required |
Description |
|---|---|---|---|
|
string |
Yes |
Object class label (e.g. |
|
object |
One of ① |
Normalized image-space bounding box ( |
|
object |
One of ① |
Pixel-space bounding box ( |
|
array[3] of number |
One of ① |
3D world position ( |
|
array[3] of number |
One of ① |
Geographic position (latitude, longitude, altitude); converted to ECEF internally |
|
array[3] of number |
One of ① |
3D object dimensions ( |
|
number > 0 |
No |
Inference confidence score for this detection |
|
integer ≥ 0 |
Yes ② |
Per-frame detection index |
|
array[4] of number |
No |
Object orientation as a quaternion |
|
number |
No |
Distance from the camera to the detection in metres |
|
array of objects |
No |
Pose keypoints when a pose estimation model is used; each entry: |
|
array of strings |
No |
Flat list of keypoint-name pairs defining connections (e.g. |
|
object |
No |
Semantic attribute bag (see Semantic Metadata Fields) |
① Location constraint: every detection must provide location in exactly one of these forms (enforced by the schema’s
oneOf):
2D image-based:
bounding_boxand/orbounding_box_px(at least one required; both may be present — if so,bounding_boxtakes precedence)3D world-space:
translation+sizeGeographic:
lat_long_alt+size(converted to ECEFtranslationinternally)
② Schema vs runtime: The JSON schema currently lists
idas optional (onlycategoryis in the schema’srequiredarray). However, the controller accessesidunconditionally at runtime and will reject detections that omit it. Always includeidin every detection object.
Semantic Metadata Fields (objects.<category>[*].metadata.<attr>)#
Field |
Type |
Required |
Description |
|---|---|---|---|
|
any |
Yes |
Detected value for this attribute (e.g. |
|
string |
Yes |
Name of the model that produced this attribute |
|
number [0, 1] |
No |
Confidence score for the detected attribute |
Example Camera Detection Message#
The following example shows a typical message published by a camera pipeline (debug fields
omitted; embedding_vector truncated for readability):
{
"id": "atag-qcam1",
"timestamp": "2026-03-26T21:01:31.486Z",
"rate": 10.03,
"objects": {
"person": [
{
"id": 1,
"category": "person",
"confidence": 0.998,
"bounding_box_px": {
"x": 419,
"y": 64,
"width": 192,
"height": 411
},
"keypoints": [
{ "name": "nose", "x": 0.122, "y": 0.157 },
{ "name": "eye_l", "x": 0.115, "y": 0.136 },
{ "name": "eye_r", "x": 0.16, "y": 0.125 },
{ "name": "shoulder_l", "x": 0.262, "y": 0.276 },
{ "name": "shoulder_r", "x": 0.602, "y": 0.198 }
],
"keypoint_connections": [
"nose",
"eye_l",
"nose",
"eye_r",
"eye_l",
"ear_l",
"eye_r",
"ear_r"
],
"metadata": {
"age": {
"label": "39",
"model_name": "age_gender"
},
"gender": {
"label": "Male",
"model_name": "age_gender",
"confidence": 0.979
},
"reid": {
"embedding_vector": "<base64-encoded string>",
"embedding_dimensions": 256,
"model_name": "torch-jit-export"
}
}
}
]
}
}
For the full schema definition, see metadata.schema.json.
External Source Input Message Format#
The Scene Controller subscribes to scenescape/external/{publisher_id}/{thing_type}
(MQTT template parameter name remains scene_id in PubSub APIs). The path id is
always the publisher (configured child scene uid or agent source_id). Scenes
attach via consumer-side bindings, not by addressing a scene inbox. See
ADR 16.
Two payload contracts share the topic, distinguished by source_id:
Configured child scene (no
source_id):{publisher_id}is the sending child’s own id. The controller looks up the child’s configured parent and staticcameraPose. Only scenes with a parent publish this hierarchy form; roots do not emit hierarchy echoes onto the external topic.Unified external source (
source_idpresent):{publisher_id}must equalsource_id. The controller binds the publisher to one or more scenes:Manual:
CONTROLLER_EXTERNAL_SOURCE_BINDINGS=publisher_id:scene_uid,...Geospatial auto-attach (interim):
reference_frame: wgs84attaches to every scene with four-corner geospatial calibration (until a footprint/handoff binder)Cache reuse: pose omitted → scenes that still hold a live cached pose for this publisher
Scene-frame poses: require a manual binding (and
CONTROLLER_TRUSTED_POSITIONING_SOURCESfor acceptance)
Messages with source_id are validated against the external_source definition in
metadata.schema.json.
This section documents the unified external-source payload contract.
To write a converter that maps a source’s native output into this contract and publishes over authenticated MQTT, see Publish Observations from an External Source Adapter.
External Source Top-Level Fields#
Field |
Type |
Required |
Description |
|---|---|---|---|
|
string (ISO 8601 UTC) |
Yes |
Time the observations (and pose, if present) were acquired |
|
string |
Yes |
Publisher id; must match the topic |
|
array |
Yes |
Observed objects, in the source’s local coordinate frame (see External Detection Object Fields); may be empty for a pose-only update |
|
object |
No |
Pose of the source’s local origin, used to transform |
External Source Pose Fields (pose)#
Field |
Type |
Required |
Description |
|---|---|---|---|
|
string |
Yes |
|
|
array[4] of number |
No |
Orientation of the source’s local origin, as a quaternion ( |
|
array[3] of number |
If |
Global position of the source’s local origin (latitude, longitude, altitude in metres) |
|
array[3] of number |
If |
Position of the source’s local origin in scene-local coordinates ( |
|
number > 0 |
No |
Estimated accuracy of the reported position in metres, if known |
|
string |
No |
Informational label for what produced this pose (e.g. |
reference_frame determines how the pose is resolved:
wgs84— A global geopose (e.g. from an onboard GNSS/INS). Requires the target scene to have valid four-corner geospatial calibration (map_corners_lla); the controller convertslat_long_altto scene-local coordinates via the scene’s LLA/ECEF transform. Rejected withscene_georeference_unavailableif the scene is not geo-referenced. Any source may publish this frame.scene— A pose already expressed in the target scene’s local coordinates. This is a privileged frame: only accepted fromsource_ids listed in theCONTROLLER_TRUSTED_POSITIONING_SOURCESenvironment variable (comma-separated list), intended for the Scenescape positioning service. Rejected withuntrusted_scene_poseotherwise. Unset or empty trusts no source (fails closed).
Note: Only position is transformed through the scene’s geospatial calibration for
wgs84poses;rotationis passed through unrotated, matching existing camera-detectionlat_long_althandling. Full ENU-to-scene orientation alignment is future work.
External Detection Object Fields (objects[*])#
Field |
Type |
Required |
Description |
|---|---|---|---|
|
string |
Yes |
Category or class of the observed object (e.g. |
|
array[3] of number |
Yes |
Position of the object relative to the source’s local origin ( |
|
string |
Yes |
Identifier the source uses to correlate this observation across messages; not a Scenescape global ID, and the controller does not map or look it up — it is passed through as the observation’s local reference |
|
array[4] of number |
No |
Rotation of the object as a quaternion ( |
|
array[3] of number |
No |
Object dimensions ( |
|
number > 0 |
No |
Source-reported confidence for this observation |
|
object |
No |
Semantic attribute bag; same structure as camera input (see Semantic Metadata Fields) |
Unlike camera detections, size is optional here: a source that cannot estimate an object’s
extent may report a point observation. Point objects (no size) remain eligible for
position-based ROI, tripwire, and sensor-tagging analytics, but are excluded from
volume/occupancy/collision analytics.
Pose Caching and Message Ordering#
A resolved pose is cached per (scene_id, source_id) for 30 seconds. Subsequent messages
from the same source may omit pose entirely to reuse the cached transform; a message with a
pose and an empty objects array updates the cache without ingesting any observations.
Messages with a timestamp older than the currently cached pose are treated as out-of-order
and ignored in favor of the newer cached transform. If no usable transform can be resolved
(no pose supplied and nothing cached, or the cached pose has expired), the message is dropped
without ingesting objects. Rejection reasons (logged, not published) include:
Reason |
Cause |
|---|---|
|
No |
|
No |
|
|
|
|
|
|
|
|
Trusted Identity by Default, with Collision Detection#
Every external-source object’s id (see
External Detection Object Fields) is trusted
directly as its global track identity (gid) by default. There is no allowlist or environment
variable to configure, and no per-source registration step: any source_id may publish and have
its objects’ ids trusted immediately. This is deliberate — requiring an operator to
pre-configure which sources are safe to trust does not scale as the number of external
sources/integrations grows.
Trusting id directly means the object bypasses Scenescape’s kinematic multi-object tracker/ReID
association entirely for that object: the source-supplied id becomes gid and stays gid for
as long as the source keeps reporting that same id in subsequent messages, exactly matching how
a UWB/RTLS tag’s own permanent hardware identifier is meant to be used. If the source stops
reporting an id, that track ages out and is dropped after the same staleness window used for
any other track that stops receiving updates — there is no special cleanup required.
Collision detection. Trusting every source’s id unconditionally would let two different
sources that happen to report the same id value silently merge two distinct physical objects
under one identity. To prevent that without requiring configuration, each id is claimed
exclusively per (scene, category): only one source_id may hold a live claim on a given id
at a time. If a second source publishes the same id while another source’s claim on it is still
live, the newly arriving, colliding object is dropped — logged as a rejection, not merged or
substituted — while any other, non-colliding objects in the same message are still ingested
normally. A claim goes stale (and can be reclaimed by a different source) after the same
identity-claim TTL used for pose-cache reuse; a source that legitimately stops publishing an
id and a different source later reusing that same id value is therefore not permanently
blocked.
What collision detection does not cover. It only detects two different sources colliding
on the same id at the same time. It cannot detect — and does not attempt to detect — a single
source reusing one of its own previously-claimed ids for a genuinely different physical
object once its earlier claim has gone stale (for example, a robot restarting and reissuing small
integer track-slot numbers that a previous, now-stale claim also used). For a source with that
kind of unstable/resettable local id scheme, a reused id will be silently accepted as if it
were a continuation of the previous object’s identity. See
Choosing a source_id below for how to
avoid this by choosing a genuinely persistent, unique identifier.
Security note: identity is trusted based on the source_id/id values present in the
message payload, not a cryptographically verified per-device credential — Scenescape’s current
MQTT authentication does not yet bind individual publishers to individual source_ids (see
ADR 16). A publisher that
can reach the broker can claim any source_id/id it chooses, subject only to the collision
check above.
Choosing a source_id (Self-Identification for Agents)#
source_id is not provisioned or registered anywhere in Scenescape ahead of time — unlike a
camera or sensor id, which must match a scene/sensor already configured in the database, an
external source simply announces itself by choosing a source_id string and publishing with it.
Because every external source’s objects[*].id is trusted as global identity by default (see
above), choosing a persistent, unique source_id and per-object id matters more here than for
most other Scenescape identifiers: the deployer/integrator is responsible for choosing values
that are:
Persistent — stable across process restarts and reboots, so a track’s identity (and any cached pose) is recognized as the same source/object next time it publishes, rather than treated as a brand-new one.
Unique — will not collide with another source’s identifier on the same scene/broker.
Recommended choices, in order of preference:
A hardware-rooted identifier already unique to the device — a serial number, a TPM-backed device UUID, or (for a UWB/RTLS tag) the tag’s own hardware/network ID. This is the strongest option because it is normally immutable and cannot be trivially changed by reconfiguring software.
The primary network interface’s MAC address — a practical, widely available choice for robots, drones, and other networked devices; it is unique per interface and typically stable across reboots. This mirrors the convention already used for sensor identifiers elsewhere in Scenescape (see the Sensor Input Message Format example,
02:42:ac:11:00:05.1).A deployer-assigned static name (for example
"drone-1","forklift-north-3") — acceptable as long as it is provisioned once per physical device and not regenerated on every boot or process restart.
Do not use a randomly generated value (for example a fresh UUID minted at process startup)
as source_id or as an object’s id: it defeats pose-cache reuse across restarts and, since
every object’s id is trusted directly as identity, means each restart creates a brand-new
identity for what should be the same physical object. Worse, for a source whose local id
scheme resets or recycles (for example, small integer track-slot numbers reissued after a
reboot), a reused id is silently treated as a continuation of the previous object’s identity
once the earlier claim has gone stale — see the collision-detection limitation above. Prefer a
hardware-rooted or MAC-based identifier specifically to avoid this.
If a robot or drone reports itself as a tracked object (for example, to visualize the
platform itself in the scene alongside objects it observes), use the same persistent identifier
described above for that object’s id — typically the platform’s own MAC address, serial
number, or device UUID — rather than a value tied to the current process/session.
Example: Agent Publishing a Global Pose and Observations#
{
"timestamp": "2026-03-26T21:01:31.486Z",
"source_id": "drone-1",
"pose": {
"reference_frame": "wgs84",
"lat_long_alt": [37.38688947, -121.96410521, 8.07],
"rotation": [0, 0, 0, 1]
},
"objects": [
{
"id": "track-42",
"category": "vehicle",
"translation": [3.2, -1.4, 0.0],
"confidence": 0.91
}
]
}
Example: Positioning Service Publishing a Scene-Local Pose#
Requires source_id (e.g. "positioning-service-1") to be listed in
CONTROLLER_TRUSTED_POSITIONING_SOURCES.
{
"timestamp": "2026-03-26T21:01:31.486Z",
"source_id": "positioning-service-1",
"pose": {
"reference_frame": "scene",
"translation": [5.0, 2.0, 0.0],
"rotation": [0, 0, 0, 1],
"provider": "positioning_service"
},
"objects": [
{ "id": "person-7", "category": "person", "translation": [1.0, 0.5, 0.0] }
]
}
Example: Pose-Only Update and Point-Object Observation#
A pose-only update refreshes the cached transform without ingesting observations:
{
"timestamp": "2026-03-26T21:01:41.486Z",
"source_id": "drone-1",
"pose": {
"reference_frame": "wgs84",
"lat_long_alt": [37.38688947, -121.96410521, 8.07],
"rotation": [0, 0, 0, 1]
},
"objects": []
}
A subsequent message reuses the cached pose and reports a point object (no size):
{
"timestamp": "2026-03-26T21:01:42.486Z",
"source_id": "drone-1",
"objects": [
{ "id": "person-3", "category": "person", "translation": [1.0, 0.5, 0.0] }
]
}
Common Output Track Fields#
Note: Sensor input (
scenescape/data/sensor/{sensor_id}) is no longer consumed by the Scene Controller. Sensor correlation is owned by the Analytics microservice — see Sensor Input Message Format and Singleton Sensor Data for the sensor input message format and how tagged data appears on scene objects.
All Scene Controller output messages include an objects array of tracked objects. Each
tracked object contains the following fields:
Field |
Type |
Description |
|---|---|---|
|
string (UUID) |
Persistent track identifier assigned by the controller |
|
string |
Object type label; same value as |
|
string |
Object class label (e.g. |
|
number |
Inference confidence of the most recent contributing detection |
|
array[3] of number |
3D world position ( |
|
array[3] of number |
3D object dimensions ( |
|
array[3] of number |
Velocity vector ( |
|
array[4] of number |
Orientation quaternion |
|
array of string |
Camera IDs currently observing this object |
|
array of objects |
Pose keypoints propagated from detections when available; each entry uses |
|
array of strings |
Flat list of keypoint-name pairs defining the skeleton edges (e.g. |
|
object |
Map of region/sensor IDs to membership metadata. Never populated by the Scene Controller — added by the Analytics microservice when it enriches this data; see note below. |
|
object |
Map of sensor IDs to timestamped readings ( |
|
number or null |
Similarity/distance value to the matched ReID embedding in the configured vector database; higher-is-better for |
|
string |
Re-ID processing state for the object. One of: |
|
array or absent |
History of UUID reassignments for this track. Each element is |
|
string (ISO 8601) |
Timestamp when the track was first created |
|
object |
Semantic attributes propagated from camera detections; present when visual analytics (e.g. age, gender, Re-ID) are configured. Same attribute structure as camera input. See note below. |
|
object |
Per-camera pixel bounding boxes ( |
Note on
metadatain track objects: Each attribute follows the structure{label, model_name, confidence?}— identical to Semantic Metadata Fields in camera input. Thereidattribute is a special case: in scene outputreid.embedding_vectoris a 2D float array ([[...numbers...]]), whereas in camera input it is a base64-encoded string.metadatais absent when no semantic analytics pipeline is configured.
Note on keypoint propagation:
keypointsandkeypoint_connectionsare optional pass-through fields from object detections. They are included in output objects when present on the contributing detection data.
Note on
similarity: This field holds the metric value returned by the ReID backend in_distanceand is evaluated by the controller using configured metric semantics. ForCOSINE(normalized vectors with backend IP/DOT), value must be abovesimilarity_threshold; for distance-style metrics such asL2, value must be belowsimilarity_threshold. A value ofnullmeans either the ReID query has not been submitted yet (pending_collection), the query found no match below the configuredsimilarity_threshold(query_no_match), or ReID is disabled (reid_disabled).
Note on
reid_statevalues:
pending_collection: Re-ID embedding collection is in progress; query has not been submitted yet.
query_no_match: Query was submitted but no database match was found.
matched: Query found a database match and the object was re-identified.
reid_disabled: Re-ID is disabled for this object lifecycle (for example due to runtime disablement).
Note on
regions/sensors: These fields are added by the Analytics microservice when it consumesscenescape/data/scene/{scene_id}/{thing_type}and republishes enriched, regulated, and region/tripwire-event output.regionsdefaults to{id: {entered: timestamp}}and gains a livedwellvalue for objects currently inside a region:{id: {entered: timestamp, dwell: seconds}}. Exit records expose the final dwell time separately as{"object": <track>, "dwell": <seconds>}in the top-levelexitedarray. See Regulated Scene Output Message Format for full format details.
Data Scene Output Message Format#
Published on MQTT topic: scenescape/data/scene/{scene_id}/{thing_type}
The Scene Controller publishes unregulated (raw) tracking results, one message per object category per scene publication cycle. Each message contains the current state of all tracked objects of that category.
Data Scene Top-Level Fields#
Field |
Type |
Description |
|---|---|---|
|
string |
Scene identifier (UUID) |
|
string (ISO 8601 UTC) |
Publication timestamp |
|
string |
Scene name |
|
number |
Current scene processing rate in Hz |
|
integer |
Cumulative count of unique detections since scene start |
|
array |
Tracked objects (see Common Output Track Fields) |
Example Data Scene Message#
{
"id": "302cf49a-97ec-402d-a324-c5077b280b7b",
"timestamp": "2026-03-26T20:49:59.642Z",
"name": "Queuing",
"rate": 9.984,
"unique_detection_count": 91,
"objects": [
{
"id": "65d49fa0-a855-46f8-bb41-4e92102c7c47",
"category": "person",
"type": "person",
"confidence": 0.999,
"translation": [2.463, 3.61, 0.0],
"size": [0.5, 0.5, 1.85],
"velocity": [-0.045, 0.012, 0.0],
"rotation": [0, 0, 0, 1],
"visibility": ["atag-qcam1", "atag-qcam2"],
"metadata": {
"age": { "label": "32", "model_name": "age_gender" },
"gender": {
"label": "Male",
"model_name": "age_gender",
"confidence": 0.904
},
"reid": {
"embedding_vector": "<embedding_dimensions-element float array>",
"embedding_dimensions": 256,
"model_name": "torch-jit-export"
}
},
"camera_bounds": {
"atag-qcam1": {
"x": 169,
"y": 4,
"width": 96,
"height": 168,
"projected": false
}
},
"similarity": null,
"reid_state": "pending_collection",
"first_seen": "2026-03-26T20:49:49.339Z"
}
]
}
Note: The example above omits
regionsandsensors, which the Scene Controller never populates on this topic (see Common Output Track Fields above). The Analytics microservice consumes this topic and republishes enriched, regulated (rate-controlled), and region/tripwire-event output — includingregionsandsensorson each object. See Analytics Service Data Formats for thescenescape/regulated/scene/{scene_id},scenescape/event/region/{scene_id}/{region_id}/{event_type}, andscenescape/event/tripwire/{scene_id}/{tripwire_id}/{event_type}message formats.