Visual reference for the MQTT broker architecture, topic namespace design, and per-category topic inventory. Read alongside the ClickUp wiki page FLYsafe_MQTT_Topic_Strategy.md.
.../alerts/{severity}, instead of publishing every alert twice (§9); and two appendices were added — a fuller eventType catalog for the frontend to build against (Appendix A), and a full example networkid payload (Appendix B).Three broker types. Each has a distinct role and a distinct set of consumers.
Publishes surveillance feeds and weather. Topics are pushed to every tenant broker on arrival. No aircraft telemetry lives here.
All aircraft telemetry, external ingest, internal coordination, and the shared feeds re-published from the platform broker. The web app subscribes here only.
Raw ingest, Flink pipeline topics, commands, dead-letter queues. Never consumed by the web app or the MQTT broker directly.
Agreed direction: the segment right after v1 is the message's category, not the tenant. Tenant stays implicit — it's already fixed by which broker you're connected to — and category becomes the one axis the whole namespace is organised around.
v2/… in parallel; consumers migrate on their own schedule. This is the version the team is finishing now — the category reorg below happens inside v1, not as a reason to reach for v2.shared · input · output · internal — see §3. This replaces the tenant ID as segment 2. It's what a tree browser groups by, and what EMQX ACL rules key off: who's allowed to publish here, and whether the web app may ever subscribe.aircraft for anything keyed by a single drone; a bare feed or service name for tenant-wide data that isn't per-drone (surveillance, infrastructure, rtm-services, inventory, dock-status, or nothing at all for events/alerts, which go straight from category to message type). Not every category needs this level — some go straight from category to feed name.+ subscribes to all drones: v1/output/aircraft/+/telemetry.aircraft-scoped: telemetry · networkid · c2links · events · proximate_aircraft · alerts. Only oi/declaration keeps an explicit context prefix — Operational Intent is a distinct domain concept, not just "another drone message," so it earns the extra segment; nothing else does.v1/{tenant}/aircraft/…), gave partner data its own root (v1/partner/…), and inserted a drones/ context level in front of every per-aircraft message type. Since none of this has shipped yet, we're correcting course before it does. Tenant moves out of the path — every tenant already has its own dedicated EMQX broker, so the connection itself tells you the tenant. And drones/ comes out too: once you're under aircraft/{serial}/, "this is drone data" is already established — repeating it before every message type added a segment without adding information. This is the same "kind-first" logic Tim's topic-convention proposal argues for (group by ingest/derived, not by aircraft): the tree only has one axis to spend, and ACLs / broker policy need it more than a redundant label does. See §3 for how partner data is represented without a dedicated segment.output is aircraft-scoped. events and alerts/{severity} are the two exceptions to the aircraft/{serial}/… pattern above — they're tenant-wide, single topics with no serial in the path at all (v1/output/events, v1/output/alerts/{severity}), same shape as the other tenant-wide feeds (infrastructure/sites, rtm-services/platform, inventory/assets). Which drone(s) and which GUI context(s) a given event or alert belongs to is carried in the payload (droneIds[], contexts[]), not the topic. See §9 for the full rationale and schema.Three categories were the starting brief — shared, input, output. Reviewing every topic already documented in this file surfaces a fourth, internal, that can't be folded into the other three without losing its ACL boundary. These four are the complete set: every topic on a tenant broker is exactly one of them, and the category alone tells you who may publish, who may consume, and whether the web app is allowed anywhere near it.
surveillance/traffic/* · surveillance/sensors/* · surveillance/fused · weather/*output. The web app never sees this category. DJI's Cloud API is the one accepted exception — it publishes at the broker root (thing/product/…) because the device can't be reconfigured to a namespaced topic; the adapter treats it as input in every other respect.gcs_telemetry (raw agent/GCS feed) · thing/product/…/osd (DJI, root exception) · anything else pre-Flink-normalisationshared are the only two categories the web app subscribes to.aircraft/{sn}/telemetry · …/networkid · …/c2links · …/oi/declaration · …/proximate_aircraft — plus tenant-wide: events (incl. TISB) · alerts/{severity} · infrastructure/sites · rtm-services/platform · inventory/assetsoutput by hand.dock-status/{sn} · asset-tracking/ops/{id} · asset-tracking/c2/{id}v1/input/#. Only Flink may publish under v1/output/# and v1/internal/#. The web app credential may subscribe to v1/shared/# and v1/output/# only — configure EMQX to reject any web app subscription to v1/input/# or v1/internal/#.v1/partner/{partnerTenant}/… root. Team decision: that's not a strong-priority structural need — the JSON payloads already carry tenant identifiers, and who's partnered with whom is exactly the kind of thing that changes over time and needs a status (pending/approved/suspended), which a topic path can't express. So a partner drone's telemetry rides the same v1/output/aircraft/{sn}/telemetry topic as any other aircraft on that broker, distinguished in the payload by a sourceTenant field; which tenants are currently partnered, and in what state, is tracked in the database and exposed to the frontend over the Partnerships API rather than via broker ACL or topic namespace. See §5 for the full picture.These topics originate on the shared broker and are re-published to each tenant broker under v1/shared/…
surveillance/traffic and surveillance/sensors, not surveillance_traffic/surveillance_sensors. MQTT wildcards match whole path segments, not substrings — there's no way to subscribe to "everything surveillance" when traffic, sensors, and fused all sit at the same level under different underscore-joined names; today that means three separate subscriptions, or a broad v1/shared/+ that also pulls in weather. Nesting them under a shared surveillance/ parent — surveillance/traffic/…, surveillance/sensors/…, surveillance/fused — makes v1/shared/surveillance/# a single clean wildcard for all of it, cleanly separate from v1/shared/weather/#. This only touches the re-published, tenant-facing form; the native topic names on the shared broker itself (left column below) are owned by the upstream surveillance systems and aren't changing — the restructuring happens in the republish step, a one-line path transform. This reopens OI-01 — see §11.| Topic (on shared broker) | Re-published as (on tenant broker) | Rate | Description |
|---|---|---|---|
| surveillance_traffic/live_mlat | v1/shared/surveillance/traffic/live_mlat | 1–5 Hz | MLAT-fused aircraft tracks. 5 Jetvision G-1090 stations — 2 clusters: CYEG area + Fort Saskatchewan, 37.2 km baseline. |
| surveillance_traffic/live_community | v1/shared/surveillance/traffic/live_community | 1–5 Hz | Community ADS-B — 989 global receivers. dump1090/readsb JSON format. Dedup by ICAO hex across feeds. |
| surveillance_traffic/live_regional_{region} | v1/shared/surveillance/traffic/live_regional_{region} | 1–5 Hz | Regional feeds: EDM (23 receivers), CAL (55 receivers), FMM (provisioned, empty on staging). |
| surveillance_traffic/live_ads | v1/shared/surveillance/traffic/live_ads | 1–5 Hz | Aggregated ADS-B feed. Slightly different schema — no r/t fields, extended emergency string. |
| surveillance_traffic/live_vision | v1/shared/surveillance/traffic/live_vision | On detect | GACM vision sensor aircraft detections. Includes emitter_device_id. Subset of ADS-B fields. |
| Topic | Rate | Description |
|---|---|---|
| v1/shared/surveillance/fused | 1–5 Hz | The Surveillance Fusion Job's deduped, merged view of every surveillance/traffic/* feed above (MLAT + community + ADS-B + vision + regional), one target list. Produced directly by Flink, not re-published from an upstream native topic — so it's already v1/shared/… shaped, no transform step. This is the tenant-wide input the DAA Proximity Job reads to build each drone's own proximate_aircraft topic (§8) — see the callout there for how a shared, non-per-aircraft feed becomes per-aircraft data. |
| Topic (on shared broker) | Re-published as (on tenant broker) | Rate | Description |
|---|---|---|---|
| surveillance_sensors/live_mlat | v1/shared/surveillance/sensors/live_mlat | 30–60 s | 5/5 online. G-1090 + G-1090 UAT hardware. Note: no per-device connected or uptime fields — station health inferred from envelope online_receivers. |
| surveillance_sensors/live_vision | v1/shared/surveillance/sensors/live_vision | 30–60 s | 2/2 online. GACM-0100-000086 + 000114. Includes altitude_mm, rtt_ms, uptime per device. altitude_mm ÷ 1000 = metres. |
| surveillance_sensors/live_community | v1/shared/surveillance/sensors/live_community | 30–60 s | 989/989 global receivers. Unique schema: has distance_miles_edm, distance_miles_fmm, distance_miles_cal per receiver — pre-computed from all three Alberta cities. |
| surveillance_sensors/live_regional_{region} | v1/shared/surveillance/sensors/live_regional_{region} | 30–60 s | EDM (23 receivers), CAL (55 receivers). Single distance_miles field from regional centre. |
| Topic | Rate | Description |
|---|---|---|
| weather/ | Varies | All weather subtopics. Full subtopic structure TBD — see Open Item OI-02. Feeds Drones — Weather tab. |
Published by FlightBinder (Flink) after normalisation. All values are canonical — correct units, decoded field names, no raw integers or sentinel values.
| Topic | Rate | App context | Key fields |
|---|---|---|---|
| telemetry | 10 Hz | Drones | lat, lng, heading, agl/aglM, asl/aslM, atl/atlM, flightMode, armed, battery, groundSpeed, vertSpeed, hAcc, vAcc, utmState |
| networkid | ~1 Hz | Drones | ASTM F3411-aligned Remote ID fields, plus a clearly-separated FlySafe extension block — see the payload breakdown below this table. |
| c2links | 1 Hz | Drones — C2 tab | per-link: label, tech, status, latency, rssi, rfScore, isLead, uplink, downlink; mobile station: lat, lng, vehicleCallsign |
| oi/declaration | On change | OI's | oiPolygon[] (Leaflet format), conformance, status, maxAgl, start/end, subscribers[], utmProvider, declarationId |
events and alerts/{severity} used to be per-aircraft topics here too — they're now tenant-wide (v1/output/events, v1/output/alerts/{severity}), grouped with the other tenant-wide feeds below. See §9 for why.networkid payload mixed genuine Remote ID broadcast fields with FlySafe-only mission-planning fields (pilot name, mission name/location, waypoints) with no marker distinguishing the two. That's a problem the moment this payload needs to satisfy a UTM/USS integration or a regulator expecting a clean F3411 message. Fix: split the payload into a standard block and a clearly-namespaced extension block, so any consumer that only wants the compliant portion can take it as-is.| Field | Notes |
|---|---|
| Standard — ASTM F3411 Network Remote ID (top-level fields) | |
| uasId, uasIdType | Basic ID Message. uasIdType one of Serial Number (ANSI/CTA-2063-A), CAA Registration ID, UTM/USS-assigned ID, or Specific Session ID. |
| uaType | Basic ID Message. Aircraft type enum (Aeroplane, Helicopter/Multirotor, Gyroplane, Hybrid Lift, Glider, etc.). |
| operationalStatus | Location/Vector Message. Undeclared · Ground · Airborne · Emergency · RemoteIDSystemFailure. |
| latitude, longitude, geodeticAltitude | Location/Vector Message. Position in WGS84. |
| height, heightType | Location/Vector Message. AGL or takeoff-relative — distinct from geodeticAltitude. |
| horizontalAccuracy, verticalAccuracy, speedAccuracy | Location/Vector Message. Standard accuracy-category enums, not raw metres. |
| speed, direction, verticalSpeed | Location/Vector Message. Ground speed, track (true north), vertical rate. |
| timestamp, timestampAccuracy | Location/Vector Message. UTC time of this state, and its accuracy. |
| selfIdText | Self-ID Message (optional). Free-text operator description, e.g. flight purpose. |
| operatorId, operatorIdType | Operator ID Message. An identifier, not a name — see extension block below for that. |
| operatorLatitude, operatorLongitude, operatorAltitude | System Message. Ground-control / operator position. |
| areaCount, areaRadius, areaCeiling, areaFloor | System Message. Declared operating area, when applicable. |
| uaClassification | System Message, optional / region-specific (e.g. EU category + class). |
FlySafe extension — nested under ext, not part of F3411 | |
| ext.pilotFullName | Human-readable name. F3411's operatorId is an identifier, not a name — this is a FlySafe addition for the C2/Events UI. |
| ext.callsign | Informal callsign — not an F3411 field. |
| ext.missionName, ext.missionLocation, ext.waypoints[] | FlySafe mission-planning concept, unrelated to Remote ID broadcast content. |
| ext.trackOrigin | Internal provenance tag (which UTM/pipeline path produced this record). |
output topics in with the true v1/shared/… surveillance/weather feeds — those are covered in §4 and aren't repeated here. Everything below is Flink-produced, tenant-scoped, and lives under output: five topics that (unlike telemetry/networkid/c2links/oi above) aren't keyed by a single aircraft serial.| Topic | Rate | App context | Key fields |
|---|---|---|---|
| events | On event | multi-context | ts, tenantId, droneIds[], contexts[], category (Comms/Mission/DAA/Power/OI/System/TISB), severity (ok/warn/crit), eventType, message, data. Tenant-wide — one stream for the whole fleet; droneIds[] is empty for events not tied to any aircraft. See §9 for the full schema and rationale. |
| alerts/{severity} | On alert | multi-context | ts, alertId, tenantId, droneIds[], contexts[], severity, category, title, message, sourceEvent, requiresAck, ackedAt, ackedBy. Tenant-wide, severity as the last path segment; droneIds: [] covers what used to be a separate broadcast topic. See §9. |
| infrastructure/sites | 30 s | Infrastructure | site.id, site.status, site.lat/lng, backhaul, latency, uptime, component.status, component.model |
| rtm-services/platform | 5 s | RTM Services | service.name, service.status, kafkaLag, flinkJobs[], cpuPct, memPct, errorRate, processingRate |
| inventory/assets | On change | Inventory | droneId, model, serial, uasType, deploymentType, ec.adsbOut, ec.adsbIn, dockId, assignedPilot |
v1/output/aircraft/+/telemetry. Partner drones use the identical topic, distinguished only by a sourceTenant field in the payload (see below). All own-tenant output: v1/output/#. All shared feeds: v1/shared/#. There's no separate "partner" wildcard to add — supporting partnerships costs zero new subscriptions, only a payload-level check in the client. events and alerts/{severity} need no wildcard at all — v1/output/events and v1/output/alerts/+ are single, already-tenant-wide subscriptions; route each message client-side by its droneIds/contexts payload fields.v1/partner/{partnerTenant}/aircraft/{serial}/… root so the topic itself signalled own-fleet vs. cross-tenant. The team's call: that's not worth a dedicated path — partnership status changes over time (pending → approved → suspended → revoked), and a topic path is a poor place to represent state that changes independently of the data flowing through it.output topics as any other drone on the receiving tenant's broker — v1/output/aircraft/{serial}/telemetry, …/networkid, …/oi/declaration. The JSON payload already carries tenant identifiers (a sourceTenant / partner_tenant field), so the frontend distinguishes "is this mine or a partner's" by reading the payload, not by parsing the topic. Which tenants are currently partnered, in what state, and since when is tracked in the partnerships database and exposed to the frontend via the Partnerships API — the broker doesn't need to know about partnerships at all.
| Topic (identical to own-fleet) | Rate | App context | What's added for a partner drone |
|---|---|---|---|
| v1/output/aircraft/{sn}/telemetry | 10 Hz | Partner Drones | Identical schema to own-drone telemetry, plus a sourceTenant field injected by the KafkaConnector transform. Web app checks this field to render with distinct visual treatment — grey icon, "Partner: {sourceTenant}" label. |
| v1/output/aircraft/{sn}/networkid | ~1 Hz | Partner Drones | Same operator context fields as own-drone networkid — pilot name, callsign, mission name, waypoints — for whichever partner-operated drone is on the map. |
| v1/output/aircraft/{sn}/oi/declaration | On change | Partner OIs | Same OI schema as own-drone declarations — polygon, conformance, max AGL, start/end, declarationId — so the partner's OI boundary renders alongside their drone position. |
partner-{B}-to-{A}-mqtt-sink KafkaConnector in the shared-kafka namespace reads {B}.telemetry.enriched and writes into this tenant's v1/output/aircraft/{serial}/… topics, routed per-serial from the Kafka message key. Partnership must be approved via the Partnerships API before messages flow — creating the connector starts streaming, deleting it stops it immediately. See Cross-Tenant Partnership Architecture for the full request → approve → suspend → resume lifecycle. What changed is only where the connector writes to (the shared output topic, not a dedicated partner root) and where partnership state lives (the database/API, not the topic tree).v1/output/# may only be published to by this tenant's Flink jobs and approved partner KafkaConnectors — never by the web app or by producers/agents (those are confined to v1/input/#).
Aircraft and OI data arrive over MQTT identically for own-fleet and partner drones (above). What differs is entirely a frontend rendering and access decision, made by reading the sourceTenant payload field and cross-referencing the Partnerships API — not anything the broker enforces topic-by-topic.
| GUI element | Own fleet | Partner drone |
|---|---|---|
| Map marker | Full-colour icon by drone type/status | Grey/outline icon + "Partner: {tenant display name}" label. Display name comes from the Partnerships API, not the raw sourceTenant slug. |
| OI polygon | Solid fill in tenant brand colour; conformance shown by fill/border colour (green conforming / red non-conforming) | Dashed/hatched border, muted fill, so ownership reads at a glance without being confused for a conformance signal. Conformance still shown, but as a small badge rather than changing the base fill. |
| Detail panel (on click) | Full detail: telemetry, C2 links, events, alerts, OI, command actions (RTL, mode change, etc.) | Position, networkid context (whatever the partner shares — pilot/callsign/mission per §5's networkid split), and OI status only. No alerts panel, no command affordances — partner drones are read-only and never alert into this tenant. |
| Filtering | Standard fleet filters (status, dock, mission) | Global "Show partner drones" toggle, plus a per-partner toggle sourced from the Partnerships panel below. All client-side against sourceTenant — no extra MQTT subscription needed. |
GET /api/partnerships — partner tenant name, status (pending/approved/suspended), and since-date — with the per-partner map visibility toggle living here. This is the one place partnership state surfaces in the GUI; the drone and OI data itself keeps flowing over the same MQTT topics regardless of what this panel shows or hides — the toggle only affects client-side rendering, not the subscription.proximate_aircraft sharing between tenants — each tenant's DAA proximity job still only reasons about its own drones against the shared surveillance feed (§8); a partner's drone is not specially injected as a DAA target.Third-party data arriving in native formats. Flink adapter jobs normalise and republish. The web app never subscribes to these topics.
| Topic (root) | Rate | Kafka output | Notes |
|---|---|---|---|
| thing/product/{sn}/osd | 1–5 Hz | {tenant}.telemetry.raw.dji | OSD = On-Screen Display. host key = dock/GCS. sub key = aircraft (airborne only). |
| thing/product/{sn}/# | Various | {tenant}.telemetry.raw.dji | All DJI message types via one wildcard subscription. |
dronesense_dock_status before attributing OSD data to a dronedock.* and c2.* canonical fields.null. Never pass 65535 to the web app — it is not a valid signal quality value.null in rth.homePt.1 = SDR primary link. 0 = 4G primary link. Map to c2.workmode string.Microservice coordination signals. Current topics use flat unversioned names. Migration to v1/internal/… namespace is recommended.
| Current topic | Proposed topic | Consumer | Purpose |
|---|---|---|---|
| dronesense_dock_status/{dock_sn} | v1/internal/dock-status/{sn} | DJI Bridge adapter | Dock↔drone serial pairing registry. Required lookup before DJI OSD can be attributed to the correct aircraft. dock_sn ≠ drone_sn. |
| asset_tracking/ops_asset/{asset_id} | v1/internal/asset-tracking/ops/{id} | Asset tracker service + infra map | Operational asset positions (trucks, docks, vehicles). Dock variant (subcategory=Dock) includes on-site weather: temperature, humidity, wind_speed, rainfall — feeds wx.groundSensors[]. |
| asset_tracking/c2_monitoring/{station_id} | v1/internal/asset-tracking/c2/{id} | C2 Monitor service | Mobile C2 station position. GRS station is vehicle-mounted on chase truck — horizontal_speed > 0 when moving. Feeds C2 tab station geolocation. Must update dynamically. |
asset_tracking/ops_asset/{dock_id} payload with subcategory=Dock carries info.temperature, info.humidity, info.wind_speed, info.environment_temperature, info.rainfall. This can populate the Drones weather tab ground sensors for sites with a dock — no separate weather API needed.info.drone_in_dock == 1 confirms drone is physically docked. info.drone_charge_state.capacity_percent gives battery while docked. info.sub_device.device_online_status gives aircraft agent connectivity.info.position_state.rtk_number (29 sats), is_fixed == 2 (RTK fixed), quality == 5 (best). This is the dock's GPS quality, not the aircraft's.A dedicated per-drone topic streams aircraft within the DAA evaluation rings to both the web app and the aircraft agent. Critical path for DAA services — each consumer gets the same message, no topic duplication.
v1/shared/surveillance/fused itself is not per-drone — it's one tenant-wide stream of every fused target (MLAT + community + ADS-B + vision + regional, deduped by the Surveillance Fusion Job), the same for every client that subscribes. It only becomes "per aircraft" here: the DAA Proximity Job joins that one shared stream against this tenant's per-drone telemetry, computes distance/bearing/closure relative to each drone individually, assigns a ring, and republishes a personalised slice to that drone's own proximate_aircraft topic. So the sharing mechanism isn't a broker feature (there's no per-aircraft filtering at the MQTT level) — it's this Flink job doing the join once, so neither the web app nor the aircraft agent has to filter the full traffic firehose themselves.| Field | Type | Description |
|---|---|---|
| ts | int ms | Evaluation timestamp (Unix ms) |
| droneId | string | Drone serial — self-describing alongside topic path |
| evalRadius / swcRadius / nmacRadius | float m | Ring radii in metres — tenant/drone-configurable, not hardcoded |
| evalCount / swcCount / nmacCount | int | Target count per ring — for UI badge display |
| targets[].hex | string | ICAO hex — primary target ID |
| targets[].callsign | string | Trimmed ATC callsign |
| targets[].lat / .lng | float deg | Target position |
| targets[].alt_baro / .alt_geom | float ft | Barometric and geometric altitude |
| targets[].gs / .track / .baro_rate | kt / deg / ft/min | Speed, heading, vertical rate |
| targets[].distM / .distNm | float | Distance from drone — primary DAA field |
| targets[].bearing | float deg | Bearing from drone to target (true north) |
| targets[].closureRate | float m/s | Closure rate — positive = approaching |
| targets[].ring | enum | EVAL / SWC / NMAC |
| targets[].isMlat / .isTisb / .trust / .feedSource | mixed | Source provenance and trust level |
Events and alerts serve different audiences and travel different pipelines. A single event may trigger zero, one, or many alerts depending on configurable routing rules.
droneIds[] and contexts[] say which drone(s) and GUI area(s) it belongs to.droneIds[]/contexts[], the same way events do. No per-aircraft subscription, no separate broadcast topic.events and alerts/{severity} used to sit under aircraft/{serial}/… like telemetry. Moving them to flat topics under output lines them up with how the other non-per-drone output feeds already work (infrastructure/sites, etc.) instead of running two competing patterns for conceptually similar "things that happen" data. It also folds the previously separate shared/alerts/broadcast topic into the general model — a platform-wide alert is just an alert with droneIds: [], not a special case. And it scales flatter: one subscription regardless of fleet size, instead of a + wildcard whose match set grows with every new drone.droneIds — for very large fleets that's more bandwidth per client than a scoped per-serial subscription would be. Reasonable for fleet sizes in the tens, worth revisiting if that changes — tracked as OI-15..../alerts topic alongside the severity-scoped one; that's gone too (§ history), since the wildcard alerts/+ already covers "give me everything" with a single topic shape published once.| Event | Alert? | Severity | Notify |
|---|---|---|---|
| DAA | |||
| utmState == EMERGENCY | YES | CRITICAL | All users |
| DAA NMAC breach | YES | CRITICAL | Ops team |
| DAA SWC breach | YES | WARNING | Ops team |
| Target enters eval ring | no | — | Event only — no alert |
| C2 links | |||
| All C2 links lost | YES | CRITICAL | Ops + pilot |
| Primary link lost | YES | WARNING | Ops team |
| Link restored | no | — | Event only |
| Power | |||
| Battery < 15% | YES | WARNING | Ops team |
| Battery < 5% | YES | CRITICAL | All users |
| Battery at 30% | no | — | Event only — logged |
| OI / Mission | |||
| OI non-conforming | YES | WARNING | Ops team |
| Mission complete | YES | INFO | Ops team |
| Flight mode change | no | — | Event only — logged |
| TISB — folded in from the old dedicated tisb topic | |||
| CIFIB connection lost | YES | WARNING | Ops team |
| CIFIB connection restored | no | — | Event only — logged |
droneIds[], or to a global banner when droneIds is empty (what used to be a separate broadcast topic). No per-aircraft subscription, no per-user routing, no user identity required.| Topic | Purpose | Who subscribes |
|---|---|---|
| v1/output/alerts/{severity} | All alerts, tenant-wide — DAA, C2, battery, OI, mission, and platform-level (droneIds: []). One topic shape serves both "give me everything" (wildcard the severity segment) and "give me only criticals" (subscribe the exact severity). | Web app (one subscription, all drones) · Selective automated consumers (exact severity, e.g. a paging integration) |
v1/output/alerts/+ — single subscription, no aircraft wildcard needed since the topic was never per-aircraft. Client routes by droneIds[]/contexts[] in the payload to the correct drone panel or a global banner.
eventType catalog in Appendix A| Field | Type | Description |
|---|---|---|
| ts | int ms | Unix timestamp (ms) when event occurred |
| tenantId | string | Tenant identifier |
| droneIds[] | string[] | Aircraft serial(s) this event concerns. Empty for a tenant-wide event not tied to any drone (e.g. a Flink job restart); one element in the common case; more than one for events involving multiple aircraft. |
| contexts[] | string[] | Which GUI area(s) to surface this in — one or more of drones · oi · surveillance · infrastructure · rtm-services · inventory (§2). Usually one; an OI event might carry both drones and oi so it shows in each place a user would look for it. |
| category | enum | Comms · Mission · DAA · Power · OI · System · TISB |
| severity | enum | ok · warn · crit |
| eventType | string | Machine-readable identifier (snake_case) |
| message | string | Human-readable event description |
| data | object | Category-specific detail fields — see examples below |
| Field | Type | Description |
|---|---|---|
| ts | int ms | Unix timestamp (ms) when alert was issued |
| alertId | string | Unique alert identifier — prefixed ID or UUID |
| tenantId | string | Tenant identifier |
| droneIds[] | string[] | Aircraft serial(s) this alert concerns. Empty array for platform-wide/broadcast alerts — replaces the old separate shared/alerts/broadcast topic; same field shape as events. |
| contexts[] | string[] | Which GUI area(s) to surface this in — same enum and multi-value rationale as the event payload above. |
| severity | enum | critical · warning · info |
| category | enum | DAA · C2 · Power · OI · System |
| title | string | Short display title for UI notification |
| message | string | Full alert message for display |
| sourceEvent | string | eventType that triggered this alert |
| requiresAck | bool | Whether user acknowledgement is required |
| ackedAt | int ms|null | Acknowledgement timestamp — null if pending |
| ackedBy | string|null | User ID who acknowledged — null if pending |
Ready-to-publish payloads for each event and alert category. Use these in MQTT Explorer, a test harness, or the EMQX dashboard to simulate flight conditions during development.
SIMTL70 · Tenant: airmarket · Per-aircraft topic prefix: v1/output/aircraft/SIMTL70/ (telemetry, networkid, c2links, oi/declaration, proximate_aircraft) · Events/alerts: tenant-wide, no serial in the topic — v1/output/events, v1/output/alerts/{severity} — replace ts values with the current Unix epoch in milliseconds when publishing.v1/output/events (all use droneIds: ["SIMTL70"] below){
"ts": 1718467200000,
"tenantId": "airmarket",
"droneIds": ["SIMTL70"],
"contexts": ["drones"],
"category": "Comms",
"severity": "warn",
"eventType": "link_primary_lost",
"message": "Primary C2 link lost — SDR offline",
"data": {
"linkLabel": "SDR",
"tech": "SDR",
"previousStatus": "active",
"currentStatus": "offline",
"backupActive": true,
"backupTech": "4G"
}
}
{
"ts": 1718467320000,
"tenantId": "airmarket",
"droneIds": ["SIMTL70"],
"contexts": ["drones"],
"category": "DAA",
"severity": "crit",
"eventType": "daa_nmac_breach",
"message": "NMAC breach — C172 within 152m",
"data": {
"targetHex": "C00F12",
"targetCallsign": "CNC172",
"distM": 152.4,
"closureRate": 14.7,
"ring": "NMAC",
"targetAltFt": 2340,
"droneAglM": 91.5
}
}
{
"ts": 1718467440000,
"tenantId": "airmarket",
"droneIds": ["SIMTL70"],
"contexts": ["drones"],
"category": "Power",
"severity": "warn",
"eventType": "battery_low",
"message": "Battery at 13% — return recommended",
"data": {
"batteryPct": 13,
"threshold": 15,
"estimatedFlightTimeSec": 210,
"voltageV": 21.6,
"currentA": 18.3
}
}
{
"ts": 1718467560000,
"tenantId": "airmarket",
"droneIds": ["SIMTL70"],
"contexts": ["drones", "oi"],
"category": "OI",
"severity": "warn",
"eventType": "oi_non_conforming",
"message": "Aircraft outside OI boundary",
"data": {
"declarationId": "oi-2024-06-15-001",
"conformance": "NON_CONFORMING",
"deviationM": 47.2,
"maxAglM": 120,
"currentAglM": 134.5,
"missionName": "Pipeline Survey North"
}
}
{
"ts": 1718468100000,
"tenantId": "airmarket",
"droneIds": ["SIMTL70"],
"contexts": ["drones"],
"category": "Mission",
"severity": "ok",
"eventType": "mission_complete",
"message": "Mission completed successfully",
"data": {
"missionName": "Pipeline Survey North",
"missionId": "msn-2024-06-15-003",
"durationSec": 1740,
"waypointsTotal": 12,
"waypointsCompleted": 12,
"pilotName": "Jordan Ellis"
}
}
{
"ts": 1718467680000,
"tenantId": "airmarket",
"droneIds": ["SIMTL70"],
"contexts": ["drones"],
"category": "System",
"severity": "ok",
"eventType": "flight_mode_change",
"message": "Flight mode changed to AUTO",
"data": {
"previousMode": "LOITER",
"currentMode": "AUTO",
"armed": true,
"flightMode": "AUTO"
}
}
{
"ts": 1718467740000,
"tenantId": "airmarket",
"droneIds": ["SIMTL70"],
"contexts": ["drones", "surveillance"],
"category": "TISB",
"severity": "warn",
"eventType": "tisb_connection_lost",
"message": "CIFIB connection lost — no TIS-B traffic feed",
"data": {
"cifibStatus": "disconnected",
"previousStatus": "connected",
"lastUpdate": 1718467700000
}
}
v1/output/alerts/{severity} — the exact severity below is the last path segment{
"ts": 1718467320000,
"alertId": "alrt_daa_7f3a9c",
"tenantId": "airmarket",
"droneIds": ["SIMTL70"],
"contexts": ["drones"],
"severity": "critical",
"category": "DAA",
"title": "NMAC Breach",
"message": "C172 (C00F12) within 152m — immediate action required",
"sourceEvent": "daa_nmac_breach",
"requiresAck": true,
"ackedAt": null,
"ackedBy": null
}
{
"ts": 1718467200000,
"alertId": "alrt_c2_2b8f1e",
"tenantId": "airmarket",
"droneIds": ["SIMTL70"],
"contexts": ["drones"],
"severity": "critical",
"category": "C2",
"title": "All C2 Links Lost",
"message": "All C2 links offline — aircraft in failsafe",
"sourceEvent": "link_all_lost",
"requiresAck": true,
"ackedAt": null,
"ackedBy": null
}
{
"ts": 1718467440000,
"alertId": "alrt_pwr_9d4c5a",
"tenantId": "airmarket",
"droneIds": ["SIMTL70"],
"contexts": ["drones"],
"severity": "warning",
"category": "Power",
"title": "Low Battery",
"message": "Battery at 13% — return to home recommended",
"sourceEvent": "battery_low",
"requiresAck": false,
"ackedAt": null,
"ackedBy": null
}
{
"ts": 1718467800000,
"alertId": "alrt_sys_3e2a1f",
"tenantId": "airmarket",
"droneIds": [],
"contexts": ["rtm-services"],
"severity": "warning",
"category": "System",
"title": "Platform Degraded",
"message": "FlightBinder latency elevated — telemetry may be delayed",
"sourceEvent": "flink_job_lag_high",
"requiresAck": false,
"ackedAt": null,
"ackedBy": null
}
v1/output/events (with droneIds: ["SIMTL70"] in the body) to verify the Events tab renders. Publish alert payloads to v1/output/alerts/{severity} (e.g. .../alerts/critical) to verify the alert panel — the topic no longer carries the serial, so droneIds in the payload is what routes it to the right panel. The Alert Router normally produces alerts automatically from events — in manual testing, publish both independently. Use MQTT Explorer or: mosquitto_pub -h emqx-mqtt.airmarket.stage.flysafe.live -t "v1/output/alerts/critical" -m '{...}'The current, live FLYsafe RTM app — v0 — subscribes to a set of legacy flat topics with no structure at all. Nothing has shipped yet against a namespaced design, which is exactly why we can finish v1, the version being implemented now, as category-first from the start rather than build a tenant-first shape and re-migrate later. This section defines the full porting path from v0 to v1 for frontend developers and the parallel-publish strategy that makes the migration zero-downtime.
v1/output/aircraft/{serial}/…, v1/shared/…, v1/input/…, v1/internal/…. The tenant is not in the path — it's already fixed by which broker you connect to. Category is explicit instead, which is what ACL rules actually need to key off.networkid/{serial} becomes v1/output/aircraft/{serial}/networkid. The context segment (drones/, oi/) maps 1-to-1 with the RTM UI navigation context — no per-serial subscription needed; one wildcard covers all drones.v1/output/aircraft/{serial}/… topics as any other aircraft, carrying a sourceTenant payload field. Which tenants are partnered, and in what state, is tracked in the database and served over the Partnerships API.v1/output/events and v1/output/alerts/{severity} carry no serial at all — one flat topic per message type, with droneIds[] and contexts[] in the payload identifying who and where it's for. Folds the old separate broadcast alert topic into the same model (§9).output, shared, input, internal. No separate partnership root: partner drones are just more output traffic, tagged in the payload. Developed and tested in parallel against the new paths during the transition window.| Legacy topic (v0) | Target topic (v1) | Priority | Notes |
|---|---|---|---|
| Own-drone telemetry — v1/output/aircraft/{serial}/… | |||
| networkid/{serial} | …/networkid | P1 | Primary normalised source — heading, AGL, pilot name, callsign, mission name, waypoints. Most critical topic to port. Wildcard: v1/output/aircraft/+/networkid. |
| flightplan/{serial} | …/oi/declaration | P1 | OI polygon (Leaflet format), conformance state (CONFORMING / NON_CONFORMING), max AGL, start/end times, declarationId. Renamed from flightplan to reflect the operational intent concept. Wildcard: v1/output/aircraft/+/oi/declaration. |
| Events & alerts — v1/output/events, v1/output/alerts/{severity} — tenant-wide, no serial | |||
| TISB Stream/{serial} | v1/output/events (category: TISB) | P2 | TIS-B CIFIB connection status no longer gets its own topic — it's folded into the generic, tenant-wide event stream as category: "TISB", identified by droneIds[] in the payload rather than a per-serial topic. See §9 for the event schema and routing rule. |
| — no legacy topic — | v1/output/alerts/{severity} | NEW | The Alert Router's output, tenant-wide. There is no legacy alert topic to port from — v0 has no alerting concept. Also absorbs what would otherwise have been a separate broadcast topic (droneIds: []). |
| Shared feeds — v1/shared/… | |||
| surveillance_traffic/{feed} surveillance_sensors/{feed} |
v1/shared/surveillance/traffic/{feed} v1/shared/surveillance/sensors/{feed} |
P2 | Payload and feed names (live_ads, live_mlat, live_community, live_regional_*) are unchanged. The republish step now also restructures the path — surveillance_traffic and surveillance_sensors become surveillance/traffic and surveillance/sensors, so v1/shared/surveillance/# is one wildcard for all of it (§4). Middleware Surveillance Fusion Job publishes to both old and new paths during the transition window. |
| External ingest — unchanged at broker root (middleware-internal, not subscribed by web app) | |||
| thing/product/{sn}/osd | v1/output/aircraft/{sn}/telemetry (produced by DJI Bridge adapter) |
P3 | DJI publishes to the root-level thing/product/ path — this does not change. The DJI Bridge adapter normalises OSD payloads into the canonical telemetry schema. The web app never subscribes to thing/product/… directly — it always consumed normalised output. |
| Partnership data — no dedicated topic, rides the output rows above | |||
| — no legacy topic — | Same as own-fleet: …/telemetry · …/networkid · …/oi/declaration + sourceTenant field |
NEW | A KafkaConnector writes an approved partner's enriched telemetry, networkid, and OI declarations into this tenant's normal output topics per-serial, tagged with sourceTenant. No new topic root, no new wildcard — see §5 for the full rationale and §3 for the "not a fifth category" decision. Partnership state (who, since when, what status) is DB-backed, exposed via the Partnerships API. |
| Topic | Context | What it enables |
|---|---|---|
| v1/output/aircraft/{sn}/telemetry | Drones | Canonical 10 Hz drone position direct from FlightBinder — lat, lng, heading, AGL, ASL, flight mode, armed state, battery, ground speed. Primary own-fleet map feed. |
| v1/output/aircraft/{sn}/c2links | Drones — C2 tab | Per-link C2 telemetry — SDR/4G label, latency, RSSI, RF score, primary/backup designation, mobile station lat/lng. Powers the C2 tab and chase-truck map pin. |
| v1/output/events | multi-context | Typed event stream (Comms / Mission / DAA / Power / OI / System / TISB), tenant-wide. Powers the Events tab, routed client-side by droneIds[]/contexts[]; feeds the Alert Router for downstream alerting. |
| v1/output/aircraft/{sn}/proximate_aircraft | Drones — DAA | DAA proximity rings — all air traffic within EVAL / SWC / NMAC radii, with distance, bearing, and closure rate per target. Powers the DAA map overlay. |
| v1/output/alerts/{severity} | multi-context | Alert stream, tenant-wide, severity as the last segment. Frontend subscribes once with v1/output/alerts/+ — no aircraft wildcard needed. Routes by droneIds[] to the correct drone panel, or a global banner when empty (absorbs the old broadcast topic). |
v1/output/aircraft/{serial}/… topics as own-fleet drones (§5), the wildcard subscriptions the frontend already needs for its own fleet cover partner drones automatically the moment a KafkaConnector starts writing. The only frontend work is (1) reading the sourceTenant payload field to decide how to render a drone, and (2) calling the Partnerships API to know which tenants are currently partnered, for UI labelling — there's nothing to subscribe to that isn't already subscribed.v1/output/aircraft/+/networkid — single wildcard covers all drones, no per-serial subscription neededv1/output/aircraft/+/oi/declaration — renamed to reflect the operational intent concept; schema extended with conformance statev1/shared/surveillance/traffic/{feed} — payload and feed names unchanged; path now nests under surveillance/ (§4)v1/output/events, filtering for category: "TISB" — no longer a dedicated topic, and no longer per-serial; the affected drone is in the payload's droneIds[]droneIds[]/contexts[]. The Alert Router derives alert topics from these events.droneIds[] to the correct drone panel, or a global banner when empty. Subscribe a narrower .../alerts/critical instead if a consumer only wants criticals.v1/output/aircraft/+/telemetry / …/networkid / …/oi/declaration wildcards already subscribed for own-fleet — check the sourceTenant payload field to render distinctly (grey icon, "Partner: {sourceTenant}" label)v1/partner/… topic segment would have told youv1/output/… / v1/shared/… path. No existing consumer breaks. Partner drone data begins appearing on v1/output/aircraft/{serial}/… the moment the first KafkaConnector is created for an approved partnership — it has no legacy path to maintain and needs no separate publish step.v1/output/… and v1/shared/… topics and calls the Partnerships API for partner status. Both versions run in parallel — v0 on legacy paths, v1 on the category-first structure — without interfering with each other.v1 is the only structure going forward until a genuine v2 schema change is needed.Unresolved items and confirmed resolutions.
| ID | Item / Resolution | Status | Owner |
|---|---|---|---|
| OI-01 | surveillance_traffic/ structure NOT changing. Parent topic with sub-topics per service. live_ads has reduced field set vs live_community — missing r, t, mlat[], rssi. Fusion Job handles both schemas. | ✅ Resolved | Middleware |
| OI-02 | Topic is weather/. Subtopic structure and payload TBD — to be provided by weather service team. | ⏳ Partial | Weather team |
| OI-03 | dronesense_dock_status produced by FLYsafe microservices AFTER OSD messages arrive. DJI Bridge must queue OSD until pairing available — do not drop. | ✅ Resolved | DroneSense |
| OI-04 | Asset IDs are human-labelled in microservice config. Stable per deployment. | ✅ Resolved | Asset tracker |
| OI-05 | GRS = Ground Radio Station. Stable per vehicle per deployment. | ✅ Resolved | C2 team |
| OI-06 | Internal topic migration to v1/internal/ namespace. Parallel publish approach confirmed. Timeline TBD. | ⏳ In progress | Middleware |
| OI-07 | DLQ monitoring — developer monitors during development. AI agent monitoring under consideration for production. | ⏳ In progress | Middleware / Ops |
| OI-08 | Partnership connector source topic: prefer {partnerTenant}.telemetry.enriched over .raw — enriched is already canonical RTTP schema, web app receives correct units and decoded fields with no additional transform. Confirm Tenant B's enriched topic is published into the shared-kafka namespace before the connector is built. | ⏳ Open | Tenant B middleware |
| OI-09 | Category set for the v1/{category}/… axis (§3): brief named three (shared, input, output); reviewing existing dock-status / asset-tracking / C2-station topics surfaced a needed fourth, internal. Team to confirm this is the complete set and that the keyword itself (vs. e.g. coordination) is final. | ⏳ Open | Middleware / Web App |
| OI-10 | Tenant dropped entirely from the topic path (was segment 2 in the prior draft) on the basis that each EMQX broker is already tenant-dedicated. Confirm no consumer genuinely needs a single federated cross-tenant subscription spanning multiple brokers — if one ever does, that's a reason to revisit, not a reason to keep tenant in every topic today. | ⏳ Open | Middleware |
| OI-11 | Partnership representation: v1/partner/… root replaced by a sourceTenant payload field on normal output topics, with partnership status/lifecycle moved to a database + Partnerships API. Confirm the API's shape (endpoint, auth, poll vs. push) with the web app team before it's built. | ⏳ Open | Middleware / Web App |
| OI-12 | networkid payload redesigned around ASTM F3411 Network Remote ID message types, with mission/pilot fields moved under a non-standard ext block (§5). Field names above describe F3411's message types, not a confirmed wire schema — verify against the exact F3411 edition/profile in use and adjust field names before implementation. | ⏳ Open | Middleware / Compliance |
| OI-13 | surveillance/fused (§4, §8) categorised as shared on the assumption the Fusion Job's merged output is identical across tenants, same as its shared inputs. Confirm whether it actually runs once centrally or per-tenant, and whether any tenant-specific filtering is ever applied — if so it belongs under output instead. | ⏳ Open | Middleware |
| OI-14 | TISB CIFIB connection status folded into the events topic as category: "TISB" (§5, §9), replacing the old dedicated drones/tisb topic. Confirm no existing consumer depends on subscribing to TISB status in isolation without pulling the full event stream. | ⏳ Open | Web App |
| OI-15 | events and alerts/{severity} moved from per-aircraft (aircraft/{serial}/…) to tenant-wide flat topics (§9, §10), with droneIds[]/contexts[] replacing topic-level scoping. Confirm this array-based payload shape is final (vs. a simpler nullable single droneId for the common one-drone case), and confirm the bandwidth trade-off — every client now receives the whole tenant's stream — is acceptable at expected fleet sizes. | ⏳ Open | Middleware / Web App |
| OI-16 | Re-published (tenant-facing) surveillance topics restructured from surveillance_traffic/surveillance_sensors to nested surveillance/traffic/surveillance/sensors (§4), refining OI-01 — the native upstream topic naming on the shared broker is unaffected, only the republish transform. Confirm no tenant-side consumer already depends on the old flat v1/shared/surveillance_traffic/… form before cutover. | ⏳ Open | Middleware |
The eventType catalog the frontend Events tab (and the Alert Router) should build against. All of these arrive on the single tenant-wide v1/output/events topic (§9) — the frontend routes by the payload's droneIds[] and contexts[], not by which topic the event was on. §9 shows six as worked examples; this is the fuller set to design the UI around now, so a new event type doesn't require a frontend release later.
oi_non_conforming, or a link-restored event to pair with each link-lost). Build the Events tab to render gracefully on an eventType or category it doesn't recognise — new ones will land over time without warning.| eventType | Category | Severity | Alert? | Notes |
|---|---|---|---|---|
| DAA | ||||
| daa_target_enters_eval | DAA | ok | no | Target enters the outer evaluation ring. Logged for the DAA target list; no user-facing alert. |
| daa_swc_breach | DAA | warn | WARNING | Target inside the self-separation (SWC) ring. |
| daa_nmac_breach | DAA | crit | CRITICAL | Near mid-air collision ring breach. |
| daa_conflict_resolved | DAA | ok | no | Target has left the ring it previously breached — pairs with the breach event so the UI can clear the alert state. |
| utm_emergency_declared | DAA | crit | CRITICAL | utmState == EMERGENCY. |
| Comms (C2) | ||||
| link_degraded | Comms | warn | WARNING | RF score / latency crosses a configured threshold without the link actually dropping. |
| link_primary_lost | Comms | warn | WARNING | Primary link down, backup still active. |
| link_all_lost | Comms | crit | CRITICAL | All C2 links down — aircraft in failsafe. |
| link_restored | Comms | ok | no | A previously-lost link (primary, secondary, or all) is back. |
| Power | ||||
| battery_level_update | Power | ok | no | Routine checkpoint (e.g. 30%) — logged, not alerted. |
| battery_low | Power | warn | WARNING | Below the tenant-configurable low threshold (default 15%). |
| battery_critical | Power | crit | CRITICAL | Below the tenant-configurable critical threshold (default 5%). |
| battery_charging_started | Power | ok | no | Dock-charging began — relevant to the Inventory/Dock UI, not the flight map. |
| battery_charging_complete | Power | ok | no | Dock-charging finished. |
| OI / Mission | ||||
| oi_non_conforming | OI | warn | WARNING | Aircraft outside its declared OI boundary. |
| oi_conforming | OI | ok | no | Back inside the boundary — pairs with oi_non_conforming to clear the alert. |
| mission_started | Mission | ok | no | Mission execution began. |
| mission_complete | Mission | ok | INFO | All waypoints completed successfully. |
| mission_aborted | Mission | warn | WARNING | Mission stopped before completion — pilot/ops abort, distinct from a normal finish. |
| System | ||||
| flight_mode_change | System | ok | no | e.g. LOITER → AUTO. |
| armed_state_change | System | ok | no | Arm/disarm transition. |
| remoteid_system_failure | System | crit | CRITICAL | F3411 operationalStatus == RemoteIDSystemFailure (§5 Appendix B) — the aircraft has stopped broadcasting valid Remote ID. |
| agent_connectivity_lost | System | warn | WARNING | Aircraft agent heartbeat lost — application-level, distinct from a C2 radio link event. |
| agent_connectivity_restored | System | ok | no | Agent heartbeat resumed. |
| TISB | ||||
| tisb_connection_lost | TISB | warn | WARNING | CIFIB connection lost — no TIS-B traffic feed. |
| tisb_connection_restored | TISB | ok | no | CIFIB connection back. |
One concrete example combining the ASTM F3411-aligned standard fields with the FlySafe ext block, per §5's field breakdown. Topic: v1/output/aircraft/SIMTL70/networkid.
{
"uasId": "1596F8B0C2A1E4",
"uasIdType": "SERIAL_NUMBER",
"uaType": "HELICOPTER_MULTIROTOR",
"operationalStatus": "AIRBORNE",
"latitude": 53.5461,
"longitude": -113.4938,
"geodeticAltitude": 723.4,
"height": 91.5,
"heightType": "AGL",
"horizontalAccuracy": "HA_10M",
"verticalAccuracy": "VA_10M",
"speedAccuracy": "SA_3MPS",
"speed": 12.3,
"direction": 274,
"verticalSpeed": 0.4,
"timestamp": "2026-07-23T14:02:11.500Z",
"timestampAccuracy": "TA_0_1S",
"selfIdText": "Pipeline inspection survey",
"operatorId": "OP-AB-2024-00341",
"operatorIdType": "CAA_REGISTRATION",
"operatorLatitude": 53.5502,
"operatorLongitude": -113.4901,
"operatorAltitude": 668.0,
"areaCount": 1,
"areaRadius": 150,
"areaCeiling": 120,
"areaFloor": 0,
"uaClassification": null,
"ext": {
"pilotFullName": "Jordan Ellis",
"callsign": "RTM-70",
"missionName": "Pipeline Survey North",
"missionLocation": "Fort Saskatchewan, AB",
"waypoints": [
{ "seq": 1, "lat": 53.5461, "lng": -113.4938, "altM": 120 },
{ "seq": 2, "lat": 53.5489, "lng": -113.4870, "altM": 120 }
],
"trackOrigin": "utm-network-rid"
}
}
ext is the ASTM F3411 Network Remote ID portion — a UTM/USS integration should need nothing past uaClassification. Everything inside ext is FlySafe-only and safe to ignore for compliance purposes. See §5 for the field-by-field breakdown and OI-12 for the confirmation this still needs against the exact F3411 edition in use.