Skip to Content
APIsTelemetry

Telemetry Wire Contract

Normative rules for telemetry frame identity, acknowledgement, and MQTT transport between a GCS adapter and the UDAI platform.


1. What this contract provides

  • Idempotency: a frame delivered twice is stored once.
  • Storage-gated deletion: the adapter deletes its local copy only once the platform has committed the frame to durable storage.
  • Liveness: every sequence number reaches a terminal outcome in bounded time, so acknowledgement can never stall permanently.

The adapter mints a local flight identity for its own encrypted queue bookkeeping and uses it as the opaque Idempotency-Key for session creation. It is not a Core telemetry field and never appears in a frame, acknowledgement or status message.


2. Frame identity

Logical frame identity is the tuple:

(asset_uuid, session_uuid, sequence_number)

This tuple is unique platform-wide and is the key the ingest ledger enforces.

ConceptFieldNotes
Logical identityasset_uuid + session_uuid + sequence_numberThe dedup key.
Correlation idevent_idGlobally unique per frame. For tracing and log correlation: not the dedup key.
Session identitysession_uuidCore-issued and required before a frame is published or replayed.
Event timetimestampThe device capture time, referred to as recorded_at. Data, never identity.
Ingest timereceived_atStamped by the platform, never sent by the adapter.

Three rules that are easy to get wrong:

  1. session_uuid is identity. A flight may begin offline before any session exists, so the adapter queues frames locally first. Before publishing or replaying them, it creates or recovers the backdated session and assigns its session_uuid; Core never receives a frame without one. One local flight maps to one session.
  2. timestamp is not identity. Two frames may share a timestamp. Deduplication never considers it.
  3. The platform never assigns or rewrites sequence_number. It only validates contiguity, gaps and duplicates.

3. Telemetry frame (adapter → platform)

Topic: udai/v1/telemetry/\{asset\}/frames · QoS 1 · JSON

3.1 Identity fields

FieldTypeRequiredDescription
schema_versionintegeryes1. See §7.
event_idUUID stringyesUnique per frame. A retransmitted frame carries the same event_id.
session_uuidUUID stringyesCore-issued session that owns the frame. The adapter creates a backdated session before offline replay, so every frame reaches the platform with one already attached. The local flight identity is retained only inside the adapter and is never included in the frame body.
sequence_numberinteger (int64)yesStarts at 1, increments by 1 per session. The adapter’s local flight store preserves it across restarts and session assignment.

3.2 Payload fields

asset_uuid, asset_uin, latitude, longitude, altitude_agl, absolute_altitude_m, heading, horizontal_speed, vertical_speed, velocity_north_mps, velocity_east_mps, velocity_down_mps, roll_deg, pitch_deg, yaw_deg, rollspeed, pitchspeed, yawspeed, airspeed_mps, groundspeed_mps, climb_mps, gps_fix_type, satellites_visible, battery_temperature_cdegc, battery_voltage0_mv, battery_voltage_v, battery_current_a, battery_remaining_percent, is_armed, is_in_air, flight_mode, status_text, timestamp.

3.3 Example

{ "schema_version": 1, "event_id": "9f2c1b7e-5a44-4c8d-9f21-6b3ad0c7e114", "session_uuid": "c1d2e3f4-1234-4abc-9000-aabbccddeeff", "sequence_number": 1843, "asset_uuid": "e7a6d7a1-ffb2-4d62-bd7a-034fb8a34a55", "asset_uin": "UIN-IN-0042", "latitude": 28.6139, "longitude": 77.209, "altitude_agl": 118.4, "heading": 214.5, "horizontal_speed": 9.2, "vertical_speed": -0.3, "battery_remaining_percent": 61, "is_armed": true, "is_in_air": true, "flight_mode": "AUTO", "timestamp": "2026-09-07T09:15:02.500Z" }

3.4 Persistence rules for the adapter

  • next_sequence_number must survive a service restart. A restart must never reset or reuse a sequence number for an active local flight/session.
  • A frame is persisted locally before it is published (step 1 of the acknowledgement flow).
  • A retransmitted frame is byte-identical to the original, including session_uuid, sequence_number and event_id.

4. Acknowledgement (platform → adapter)

Topic: udai/v1/telemetry/\{asset\}/acks · JSON

The only message the adapter needs in order to free local storage.

{ "type": "telemetry_ack", "schema_version": 1, "session_uuid": "c1d2e3f4-1234-4abc-9000-aabbccddeeff", "highest_contiguous_sequence": 1842, "committed_at": "2026-09-07T09:15:03.412Z" }
FieldTypeDescription
typestringAlways telemetry_ack.
schema_versioninteger1.
session_uuidUUID stringThe session this acknowledgement covers.
highest_contiguous_sequenceinteger (int64)Every sequence from 1 to this value inclusive has reached a terminal outcome.
committed_atRFC 3339 timestampWhen the resolving transaction committed.

4.1 Semantics

  • Cumulative. On receipt the adapter deletes every locally held frame with sequence_number ≤ highest_contiguous_sequence for that session. The adapter may use its local flight identity to find the queue rows, but the acknowledgement is session-scoped.
  • Published only after commit. The acknowledgement is emitted after the durable transaction commits, never before.
  • Monotonic. The value never decreases for a session. An adapter receiving a lower value than one already applied must ignore it.
  • Idempotent and self-healing. A lost acknowledgement needs no recovery protocol: the next one supersedes it. The platform also re-publishes the current value whenever it detects a duplicate, so a lost final acknowledgement is recovered even when no newer frame follows.
  • Not a validity signal. A sequence counts toward highest_contiguous_sequence once it reaches any terminal disposition (§6): including a frame that was rejected as invalid. The adapter does not need to know which; the platform holds the body. This is what stops one bad frame stalling an entire flight.

⚠️ Do not confuse this with the legacy WebSocket telemetry_ack, which carried status: "received" and meant only that a socket accepted bytes. That message is superseded and must never be treated as a deletion signal. Deletion is gated exclusively on the message above.


5. Status and source-gap declarations (adapter → platform)

Topic: udai/v1/telemetry/\{asset\}/status · JSON

Two message types share this topic, discriminated by type.

5.1 adapter_status

Routine health reporting.

{ "type": "adapter_status", "schema_version": 1, "session_uuid": "c1d2e3f4-1234-4abc-9000-aabbccddeeff", "connection_state": "connected", "last_acknowledged_sequence": 1842, "queue_depth_frames": 120, "queue_depth_bytes": 245760, "queue_pressure": "normal", "reported_at": "2026-09-07T09:15:04.000Z" }
FieldValues
connection_stateconnected · reconnecting · offline
queue_pressurenormal · elevated · decimating · discarding

queue_pressure escalates through the overflow policy: elevated on crossing the high-water mark, decimating once the retained sample rate has been reduced, discarding once frames are being dropped.

5.2 source_gap

The declaration that keeps acknowledgement alive when the adapter has had to discard data locally. Without it, a locally discarded range would stall highest_contiguous_sequence for the remaining life of the session.

{ "type": "source_gap", "schema_version": 1, "session_uuid": "c1d2e3f4-1234-4abc-9000-aabbccddeeff", "from_sequence": 100, "to_sequence": 500, "reason": "queue_overflow", "declared_at": "2026-09-07T09:14:58.000Z" }
FieldTypeDescription
session_uuidUUID stringThe Core session that owns the discarded range. The session must already exist.
from_sequence / to_sequenceinteger (int64)Inclusive range of sequences the adapter has discarded and will never send.
reasonstringqueue_overflow today. Extensible.
declared_atRFC 3339 timestampWhen the discard happened.

Rules:

  • The adapter declares a range only after it has stopped publishing those sequences, and only once the session exists. A range discarded while the flight is still offline is held back until the backdated session is created, then declared.
  • The adapter repeats the declaration until an acknowledgement covers the range. It is idempotent.
  • The platform verifies the declaring client certificate owns the asset and the referenced session, then records the range as abandoned using insert-if-absent semantics: a sequence already stored is never overwritten by a declaration.
  • Declarations are alerted and retained for audit. They are the only legitimate source of a permanent gap, and platform metrics report them separately from loss in transit.

6. Terminal dispositions (platform-internal)

Every sequence the platform learns about resolves to exactly one terminal disposition in the ingest ledger. These do not appear on the wire: the adapter only ever sees highest_contiguous_sequence: but they are listed here because they define what that number means.

DispositionMeaningBody retained in
storedValid frame, committed to telemetry_frames.hypertable
rejectedFailed validation: schema, identity or authorisation.quarantine store
quarantinedAccepted but held for inspection rather than storage.quarantine store
failedThe platform could not process the frame after exhausting its retry budget. An operational fault, not a validation outcome.quarantine store
abandonedNever received: declared discarded by the adapter under §5.2.none, no body exists

Every sequence is either absent from the ledger or resolved to one of these dispositions. A frame that enters the platform’s processing path always ends in a terminal ledger row: otherwise the deletion gate would stall permanently.


7. Versioning

schema_version is an integer carried on every message in this contract. It is 1.

  • Additive changes: new optional fields: do not increment it. Both sides ignore fields they do not recognise.
  • Breaking changes: removing a field, changing a type, or changing the meaning of an existing field: increment it.
  • The platform accepts frames at the current version and the immediately preceding version.
  • The adapter must not reject an acknowledgement carrying a schema_version higher than it recognises if it can still read session_uuid and highest_contiguous_sequence; those two fields are stable by contract.

8. Transport bindings

MessageTopicDirectionQoS
Telemetry frameudai/v1/telemetry/\{asset\}/framesadapter → platform1
Acknowledgementudai/v1/telemetry/\{asset\}/acksplatform → adapter1
Status / source-gapudai/v1/telemetry/\{asset\}/statusadapter → platform1

MQTT 5 over mTLS on port 8883. Topics are keyed by asset, not by organisation: organisation ownership can change (assets can be transferred between organisations) while a certificate cannot, so nothing that changes independently of the asset is encoded in the topic or the certificate. The broker derives the \{asset\} segment from the certificate itself, never from what the client claims, so one asset cannot publish or subscribe under another asset’s segment. A decision that depends on organisation membership uses a live database lookup keyed on the asset instead.

udai/v1/commands/\{asset\} is reserved. Broker ACLs for command-and-control are withheld until a dedicated threat model is approved.

8.1 Ingest paths

Frame identity fields in §3 apply on MQTT, WebSocket, and REST ingest. Acknowledgement and status messages use the MQTT topics in the table above. All ingestion paths pass through the same idempotent ingest boundary and the same ledger.


9. Example: the full round trip

1 Adapter persists frame 1843 locally (encrypted SQLite) 2 Adapter publishes to .../frames at QoS 1 -> broker PUBACK 3 Platform resolves cert identity, validates the frame 4 Platform publishes to the durable stream -> stream ack 5 Platform commits ledger row + telemetry_frames row in one transaction 6 Platform publishes to .../acks: { "type": "telemetry_ack", "session_uuid": "c1d2…", "highest_contiguous_sequence": 1843, "committed_at": "…" } 7 Adapter deletes all local frames <= 1843 for that session

Steps 2 and 4 are observable progress only. Step 5 is the only one that permits deletion. The adapter’s local queue must therefore be sized for the worst-case interval between publish and step 6: the storage flush interval plus margin: and, more importantly, for the maximum expected offline period.

Last updated on