tenant.drones Postgres, exposing it through APIs that the frontend uses today and MCP agents will use tomorrowOne Aircraft Custodian per drone, per tenant. The Custodian maintains the dTwin. State lives in tenant.drones Postgres. Everything else — the frontend, AI agents, surveillance services — reads and writes the dTwin through APIs.
Naming note: the Aircraft Custodian is the entity we previously called the Aircraft Agent. Same concept, new name — chosen because the entity does more than "agent" suggests: it actively curates and maintains the dTwin as a custodian of record, not merely a passive translator of telemetry.
The dTwin is no longer treated as “a retained MQTT topic” first and a database row second. It is the canonical record of an aircraft’s current state in tenant.rtm-app-db.drones, maintained by the Aircraft Custodian via API calls, and surfaced to consumers (frontend app, AI agents, surveillance, evidence) through a stable REST API. MCP wraps that same API so future agents interact with the state through the same contract the frontend uses today.
The Aircraft Custodian is split into two Docker microservices: Custodian Software (one process per drone) and Custodian APIs (the shared REST layer). Between them sits RTTP — Real Time Telemetry Processing, the central telemetry hub. All GCS Plugins and the Cloud Broker stream telemetry into RTTP; RTTP (encompassing Flink event processing) normalises and routes data to the Custodian APIs, which write the dTwin to tenant.drones. The same API contract is consumed by the browser UI, MCP agents, and the Flight_Live microservice.
tenant.drones, kept current by the Custodian. Queryable, joinable, durable — the basis for all downstream consumers.tenant.drones current. Other systems read state from there.(tenant, serial). Callsign, ICAO hex, TC registration are properties, not keys.This is the structural view: where drone records originate, how they enter tenant.drones, and where identity propagates. It corresponds directly to the architecture whiteboard from the Jun 15 Daily Technical Review.
rtm-app-db.drones. It writes only DLB-sourced columns; RTM-managed columns are left for the Custodian and operators to manage.(tenant, serial). Carries both DLB-sourced fields (read-only via sync) and RTM-managed fields (writable via Custodian APIs).tenant.drones — that is its primary job. Telemetry enters via the GCS Plugins (§3), gets normalized, and the Custodian writes state through the API.<tenant>-<SN>. This is auth-only — no drone metadata lives here.An Aircraft Custodian is the entity inside FLYsafe.live RTM responsible for representing one specific drone. There is exactly one Custodian per drone per tenant. The Custodian carries three formal obligations, each independently observable through the state it maintains in tenant.drones.
Single point of drone identity in RTM. The Custodian maintains the canonical row in tenant.drones. All consumers — Flink, RTM Map, AI agents, evidence services — read identity through the same APIs.
Observable via: GET /drones/{serial}; row presence; agent_version field.
Honest telemetry flow with explicit failure declaration. The Custodian identifies which mechanism is delivering data for its drone, monitors the rate that source is producing, and updates its health state the moment the rate drops or latency exceeds the drone’s expected envelope (§3.6). When the source disconnects, the Custodian sets adapter_health.connected = false and data_source_status = DISCONNECTED.
Observable via: adapter_health.connected, last_publish_ago_ms, data_source_status, data_source_rate_hz, data_source_latency_ms_p95.
Maintains canonical drone state — position, attitude, battery, mission binding, ADS-B status, Remote ID, ownership flag, OI defaults. State changes flow from the GCS Plugin into the database via Custodian APIs.
Observable via: timestamps.source_ts, timestamps.ingested_at, updated_at in tenant.drones.
The data path into the Aircraft Custodian is a plugin embedded inside the operator’s GCS — not a separate edge container the operator has to install and manage. Two plugin form factors cover the bulk of our drone fleet:
A native plugin loaded inside Mission Planner. Taps the MAVLink stream the operator is already receiving from the autopilot, normalizes selected messages (GLOBAL_POSITION_INT, ATTITUDE, BATTERY_STATUS, MISSION_CURRENT, ADSB_VEHICLE, etc.), and forwards them to the Custodian API as HTTPS POSTs at a configurable rate.
Why a plugin and not a separate container: Mission Planner is already running on the operator’s workstation during every flight. We piggyback on the existing tooling and credentials. No second installer, no separate auth, no extra network hop.
What it sends: position, attitude, velocity, battery, GPS quality, flight mode, mission progress, ADS-B observations, link health, alerts (mode changes, RTL, failsafe).
A Chromium browser extension that activates when the operator has a web GCS open (DJI FH2 today, others in future). It reads telemetry and alerts from the GCS’s exposed web APIs (or as a fallback, the GCS’s observable DOM/WebSocket traffic), normalizes it, and forwards to the Custodian API as HTTPS POSTs.
Why a browser extension: DJI FH2 is a web app. The operator is already authenticated and connected to the live MQTT/REST surface DJI exposes. The plugin rides along inside the same browser session — we don’t need to set up a separate cloud microservice with DJI credentials per drone.
What it sends: position, mode, battery, mission state, payload status, gimbal state, alerts, ADS-B observations (where exposed by the GCS).
Prior versions described the integration as a Python Aircraft Agent container per drone, with adapters for MAVLink/DJI/DroneSense/ADS-B. v1.6 moves the integration point into the operator’s GCS. The Custodian itself becomes a server-side component that takes API input from the plugin, normalizes/persists state, and exposes APIs to consumers. Benefits:
| v1.4–v1.5 (edge agent) | v1.6 (GCS plugin) |
|---|---|
| Container deployed per drone (Docker image with adapters) | Plugin loaded inside Mission Planner / Chromium where operator already works |
| Operator must install/manage edge runtime | Plugin distributed via Mission Planner’s plugin loader / Chrome Web Store |
| Separate auth for edge agent | Operator’s existing GCS session carries the identity |
| Second network hop — MAVLink → agent → MQTT broker | One hop — GCS → HTTPS POST to Custodian API |
| State lived in retained MQTT topics | State lives in tenant.drones Postgres; APIs are the contract |
The Aircraft Custodian doesn’t just maintain telemetry-derived state — it’s also the system of record for how each aircraft connects. The protocol-specific configuration for every supported telemetry source (MAVLink autopilot, DJI FlightHub 2, DJI Cloud API, ADS-B) is stored in tenant.drones and exposed through the Custodian APIs. This is what makes FLYsafe.live RTM a centrally-managed fleet platform rather than a collection of one-off integrations.
The Custodian calls the APIs to obtain its own configuration at boot — GET /drones/{serial}/config returns the full configuration document including which protocol adapter to load and its connection parameters. The Custodian doesn’t carry hardcoded protocol details; everything is data, served from tenant.drones.
The Custodian provides protocol configuration through the API surface — operators (via the RTM Map UI) and AI agents (via MCP) read and update these configurations through the same APIs. One central place to manage every aircraft’s connection, regardless of vendor.
For: ArduPilot, PX4, Sentaeros 6
Configuration: autopilot host/port, MAVLink dialect, heartbeat interval, reconnect delay, system ID, component ID filter, message rate overrides
Served via the Mission Planner Plugin’s configuration endpoint
For: DJI fleets managed via FH2 web GCS
Configuration: FH2 workspace ID, device alias, MQTT broker endpoint, REST API base URL, telemetry topic filter, OSD message subscription set
Served to the Chrome GCS Plugin on session establish
For: Direct DJI Cloud API integration (server-to-server, no FH2)
Configuration: Cloud API endpoint, application credentials reference, device binding token, telemetry stream selection, command channel enablement
Used when a drone is enrolled with DJI Cloud rather than FH2
For: Per-drone ADS-B provisioning intent
Configuration: ADS-B In enabled, ADS-B Out enabled, surrogate mode, expected callsign, ICAO hex mode (dynamic / ADDRt), TIS-B rebroadcast participation (tisb_rebroadcast_enabled — when set, the Custodian rebroadcasts this aircraft’s own telemetry into the TIS-B feed so ground-based ADS-B receivers can see it)
Drives both CIFIB upstream gating and Flight_Live ownership tagging
{ "credential_ref": "vault://airmarket/dji-cloud/app-key" }), not by value. Actual secret material is held in a secrets store and resolved at runtime by the Custodian using its tenant-scoped service identity. tenant.drones never holds plaintext credentials.
Every Custodian is responsible for knowing whether data is actually arriving from the aircraft, and at what rate and latency. A silent source is worse than a failed source, because a silent source looks the same as a healthy one until an operator notices the position hasn’t moved. The Custodian’s Streaming obligation (§2) is discharged by continuously monitoring the source and surfacing its health through the state that lives in tenant.drones.
For each drone, the Custodian identifies and records:
data_source_mechanism.data_source_rate_hz.data_source_latency_ms_p50 / data_source_latency_ms_p95.publish_rate_hz and its maximum acceptable latency (data_source_max_latency_ms). Deviations from this envelope are what trigger status changes.data_source_status: HEALTHY, DEGRADED, STALE, DISCONNECTED) that summarizes the above for downstream consumers that don’t want to interpret raw metrics.| Status | Trigger | What downstream consumers do |
|---|---|---|
| HEALTHY | Rate within ±20% of publish_rate_hz and p95 latency below data_source_max_latency_ms. |
Position on map is authoritative. OI Automation uses it directly. |
| DEGRADED | Rate is below expected but source is still delivering, or p95 latency has exceeded threshold but data still arriving. | Map shows a warning halo around the icon. OI Automation continues but flags OIs generated during degradation. OCC is notified. |
| STALE | No message received in the last N × the expected interval (default N = 5). | Map suppresses live position; renders last-known position with a stale marker. OI Automation pauses regeneration. Alert raised. |
| DISCONNECTED | Source has explicitly disconnected (plugin closed, webhook subscription lost, Cloud Broker session dropped, ADS-B feed dropped). | Same as STALE plus the map badge changes to indicate the drone is off-network. Mission binding held; operator can re-attach source. |
data_source_status is the roll-up; per-source detail is available from RTTP/Flink’s own source-tracking state and surfaced at GET /drones/{serial}/data-source/history for post-flight review (§5J).
tenant.dronesThe dTwin is a row in tenant.drones. The Custodian maintains it. Everything that wants to know the current state of an aircraft reads through the API surface that sits on top of this table.
The table below maps every field the FLYsafe.software DLB exposes for a drone to its role in the Aircraft Custodian. Stored in dlb_drones fields are persisted verbatim in drones.dlb_drones and surfaced through the v_dtwin view — they are never copied into drones.drones. Seeds RTM default fields supply initial values the Import process seeds into drones.drones on first insert; operators may later override via Custodian APIs. FK link fields are written to drones.drones once to establish the foreign key relationship.
| DLB Field (exact name) | Example Value | Role | Maps to / Note |
|---|---|---|---|
guid |
02150819-48AC-A3C6-… |
FK link | PK in drones.dlb_drones; stored as dlb_guid in drones.drones as a FK on first insert. Links the two tables. All other DLB identity fields are read from dlb_drones via v_dtwin using this key. |
serial_number |
14334 |
FK link | Also written to drones.drones on first insert — forms the composite PK (tenant_id, serial_number), the binding key across all systems. |
name |
AIRTL01 |
Stored in dlb_drones | Exposed as drone_name in v_dtwin. Not copied to drones.drones. |
brand |
UAVSystemsinternatinoal |
Stored in dlb_drones | Aircraft manufacturer. Available via v_dtwin. |
model |
UAVSYSTEMSX6C |
Stored in dlb_drones | Aircraft model identifier. Available via v_dtwin. |
drone_type |
Hexacopter |
Stored in dlb_drones | Frame classification (Hexacopter, Fixed-wing, etc.). Available via v_dtwin. |
identification_number |
C-2109072432 |
Stored in dlb_drones | TC / regulatory registration. Exposed as TC_Registration in v_dtwin. Not copied to drones.drones. |
inventory_number |
(empty in example) | Stored in dlb_drones | Operator-assigned callsign used for ADS-B routing via AADMS. Exposed as Callsign in v_dtwin. Not copied to drones.drones. |
status |
Airworthy |
Stored in dlb_drones | Airworthiness status (Airworthy, Grounded, etc.). Available via v_dtwin. |
max_horizontal_speed |
15 |
Seeds RTM default | Seeds horizontal_speed_ms (m/s) on first import. Operator may override via PUT /drones/{serial}/oi-defaults. |
max_vertical_speed |
10 |
Seeds RTM default | Seeds vertical_speed_ms (m/s) on first import. Operator may override via PUT /drones/{serial}/oi-defaults. |
hardware_version |
Pixhawk Cube |
Stored / informational | Flight controller model. Useful for adapter type selection guidance (Pixhawk → MAVLink). |
firmware_version |
4.0.7 |
Stored / informational | Autopilot firmware version at time of DLB registration. |
propulsion_type |
ELECTRIC |
Stored / informational | Propulsion category (ELECTRIC, ICE, HYBRID). |
payload_capacity |
5 |
Stored / informational | Maximum rated payload in kg. |
weight |
5kg |
Stored / informational | Empty aircraft weight. |
purchase_date |
2021-03-01T00:00:00.000Z |
Stored / informational | Aircraft acquisition / commissioning date. |
max_flight_time |
0 |
Stored / informational | Rated endurance in minutes (0 = not set in DLB). |
insurable_value |
0 |
Stored / informational | Insurance / replacement value in local currency. |
color |
black |
Stored / informational | Aircraft airframe colour. |
company_guid |
9D1F94A9-97B3-… |
Stored / informational | DLB company / tenant identifier for reconciliation. |
user_guid |
FE9F94FE-3E9C-… |
Stored / informational | DLB user who registered the drone. |
controller_serial_number |
(empty) | Stored / informational | Primary RC controller serial number. |
controller_serial_number2 |
(empty) | Stored / informational | Secondary RC controller serial number. |
flight_controller_serial_number |
(empty) | Stored / informational | Flight controller board serial number. |
tech_number |
(empty) | Stored / informational | Internal technical reference number. |
notes |
(empty) | Stored / informational | Free-form notes field from DLB. |
drones.dlb_drones (raw staging table). A view (drones.v_dlb_drones) renames the two display identifiers (inventory_number → Callsign, identification_number → TC_Registration) for operator-facing display and reporting. The main Custodian dTwin table (drones.drones) carries a curated subset of DLB fields under RTM-friendly names alongside RTM-managed configuration and live telemetry state.
drones.dlb_drones)Stores every field from the DLB API with column names exactly as they arrive. This is the upsert target for the Import process (Steve Smith). No renaming — the schema matches the DLB source verbatim, making forward sync and any future column additions straightforward.
-- drones.dlb_drones · /tenant/rtm-app-db/schema/dlb_drones.sql -- Raw DLB import table. Column names match the DLB API verbatim. -- Written by the Import process (Steve Smith). Never modified by RTM side. CREATE TABLE IF NOT EXISTS drones.dlb_drones ( -- DLB primary identity (exact field names from DLB API) guid TEXT NOT NULL, -- DLB drone UUID serial_number TEXT NOT NULL, name TEXT, -- drone display name brand TEXT, model TEXT, drone_type TEXT, status TEXT, -- e.g. Airworthy, Grounded color TEXT, -- Regulatory / operational identifiers identification_number TEXT, -- TC registration (AS TC_Registration in v_dtwin) inventory_number TEXT, -- operator callsign (AS Callsign in v_dtwin) -- Hardware & firmware hardware_version TEXT, -- e.g. Pixhawk Cube firmware_version TEXT, -- e.g. 4.0.7 flight_controller_serial_number TEXT, controller_serial_number TEXT, controller_serial_number2 TEXT, -- Performance specs (stored as TEXT to match DLB source; cast on use) max_horizontal_speed TEXT, -- seeds horizontal_speed_ms in drones.drones max_vertical_speed TEXT, -- seeds vertical_speed_ms in drones.drones max_flight_time TEXT, -- rated endurance (minutes) payload_capacity TEXT, -- kg weight TEXT, propulsion_type TEXT, -- ELECTRIC | ICE | HYBRID -- Ownership & financial company_guid TEXT, user_guid TEXT, insurable_value TEXT, purchase_date TEXT, -- ISO-8601 string from DLB; cast on use tech_number TEXT, notes TEXT, -- Import bookkeeping imported_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (guid) ); CREATE UNIQUE INDEX IF NOT EXISTS idx_dlb_drones_serial ON drones.dlb_drones (serial_number);
drones.drones)The canonical per-drone RTM state table maintained by the Aircraft Custodian. Holds only RTM-specific data — configuration, live state, and the link key (dlb_guid) back to drones.dlb_drones. All DLB identity fields (name, brand, model, callsign, TC registration, etc.) live exclusively in dlb_drones and are accessed via the v_dtwin view. Speed defaults are seeded from dlb_drones on first insert and may be overridden by the operator.
-- /tenant/rtm-app-db/schema/drones.sql -- One row per drone per tenant. Maintained by the Custodian. PK = (tenant, serial). -- DLB identity data lives in drones.dlb_drones; join via dlb_guid. Use v_dtwin for the full dTwin record. CREATE SCHEMA IF NOT EXISTS drones; CREATE TABLE drones.drones ( -- Core identity tenant_id text NOT NULL, serial_number text NOT NULL, dlb_guid text NOT NULL, -- FK → drones.dlb_drones.guid -- Custodian configuration (RTM-managed; written via Custodian APIs) adapter_type text DEFAULT 'mavlink'::text NOT NULL, -- mavlink | dji_fh2 | dronesense adapter_config jsonb DEFAULT '{}'::jsonb NOT NULL, publish_rate_hz int4 DEFAULT 4 NOT NULL, -- OI defaults & FLYplan automation (speeds seeded from dlb_drones on first insert) oi_automation_enabled bool DEFAULT false NOT NULL, -- pilot-granted authority; see §5A (v1.10) horizontal_speed_ms float8 DEFAULT 15.0 NOT NULL, -- seeded from dlb_drones.max_horizontal_speed vertical_speed_ms float8 DEFAULT 6.0 NOT NULL, -- seeded from dlb_drones.max_vertical_speed -- Flight Volume (FV) / Contingency Volume (CV) buffers (v1.8; supersedes the v1.6–v1.7 scalar -- flight_volume_m / contingency_vol_m fields. Horizontal only as of v1.12 — vertical separation -- from other traffic is handled by the NMAC/SWC crewed/uncrewed minima below, not by FV/CV.) fv_buffer_m float8 DEFAULT 56.0 NOT NULL, -- horizontal buffer around flight path cv_buffer_m float8 DEFAULT 150.0 NOT NULL, -- additional horizontal buffer past FV edge -- OI geometry parameters (see OI Configuration — New Required Fields below) oi_automation_square_m float8 DEFAULT 700.0 NOT NULL, -- OI square side (m); valid 350–1400 oi_automation_radius_m float8 DEFAULT 400.0 NOT NULL, -- OI radius (m); valid 200–800 oi_automation_amend_min_trigger_m float8 DEFAULT 100.0 NOT NULL, -- OI amend min trigger (m); valid 50–200 oi_automation_amend_max_alt_agl_m float8 DEFAULT 300.0 NOT NULL, -- OI amend max altitude AGL (m) oi_automation_amend_enabled bool DEFAULT false NOT NULL, -- Flight monitoring (v1.10) — radius within which a mission waypoint is considered reached. -- Respected by Flink flight-monitoring jobs when advancing current_waypoint_index. waypoint_buffer_m float8 DEFAULT 50.0 NOT NULL, -- valid 5–500 -- Default pilot binding (v1.7) — pre-filled pilot when a mission is created without explicit selection default_pilot_name text NULL, default_pilot_fs_guid text NULL, -- FLYsafe.software pilot GUID -- Separation defaults (v1.7; split into crewed/uncrewed × horizontal/vertical in v1.12) — -- per-drone conflict-evaluation minima, metres. Crewed = manned traffic; uncrewed = other drones. nmac_h_crewed_m float8 DEFAULT 150.0 NOT NULL, -- NMAC horizontal, crewed traffic nmac_v_crewed_m float8 DEFAULT 30.0 NOT NULL, -- NMAC vertical, crewed traffic nmac_h_uncrewed_m float8 DEFAULT 15.0 NOT NULL, -- NMAC horizontal, uncrewed traffic nmac_v_uncrewed_m float8 DEFAULT 8.0 NOT NULL, -- NMAC vertical, uncrewed traffic swc_h_crewed_m float8 DEFAULT 1500.0 NOT NULL, -- SWC horizontal, crewed traffic swc_v_crewed_m float8 DEFAULT 150.0 NOT NULL, -- SWC vertical, crewed traffic swc_h_uncrewed_m float8 DEFAULT 150.0 NOT NULL, -- SWC horizontal, uncrewed traffic swc_v_uncrewed_m float8 DEFAULT 30.0 NOT NULL, -- SWC vertical, uncrewed traffic eval_distance_m float8 DEFAULT 1852.0 NOT NULL, -- Evaluation ring (~1 NM) -- ADS-B provisioning adsb_in_enabled bool DEFAULT false NOT NULL, adsb_out_enabled bool DEFAULT false NOT NULL, adsb_surrogate_enabled bool DEFAULT false NOT NULL, tisb_rebroadcast_enabled bool DEFAULT false NOT NULL, -- enables rebroadcast of this aircraft’s own telemetry to TIS-B -- Conspicuity / ADS-B detail (v1.13) — provisioning detail behind the flags above. -- ec_callsign and ec_tc_reg are NOT columns here: they are dlb_drones.inventory_number -- and dlb_drones.identification_number respectively, already exposed as Callsign / -- TC_Registration in v_dtwin. The RTM Map's "ec_*" fields are just local aliases for those. ec_adsb_hex text NULL, -- static ADS-B ICAO hex; set when adsb_hex_mode = 'static' adsb_out_device text NULL, -- transponder model, e.g. Ping978ec | Ping2020i | SkyEcho 2 | TISB | other adsb_hex_mode text DEFAULT 'dynamic'::text NOT NULL, -- dynamic (ADDRt) | static (user-entered, see ec_adsb_hex) surveillance_mode text NULL, -- descriptive surveillance method, e.g. "MLAT + ADS-B", "ADS-B + Vision" daa_enabled bool DEFAULT false NOT NULL, -- Detect-and-Avoid active for this drone broadcast_binding text NULL, -- e.g. Bound | Unbound — binding status between this drone and its broadcast identity networkid_streaming_enabled bool DEFAULT false NOT NULL, -- gates NetworkID into RTM viz + DAA; independent of tisb_rebroadcast_enabled -- Observed ADS-B state (read-only, v1.13) — from surveillance feeds, not the provisioning intent above adsb_observed_broadcasting bool NULL, adsb_observed_last_icao_hex text NULL, adsb_observed_last_seen_by_cifib_at timestamptz NULL, -- last seen by the CIFIB upstream gating pipeline -- Ownership flag (v1.5: replaces ghost extraction; see §8) ownership_flag_enabled bool DEFAULT false NOT NULL, -- Data source monitoring (v1.7) — the Custodian’s vigilance obligation; see §3.6 data_source_mechanism text NULL, -- mp_plugin | chrome_fh2 | dji_cloud | adsb | multi data_source_rate_hz float8 NULL, data_source_latency_ms_p50 float8 NULL, data_source_latency_ms_p95 float8 NULL, data_source_status text NULL, -- HEALTHY | DEGRADED | STALE | DISCONNECTED data_source_max_latency_ms int4 DEFAULT 2000 NOT NULL, -- expected envelope data_source_last_seen_at timestamptz NULL, -- Current flight (v1.9) — the mission this drone is executing right now. -- Populated by FLYsafe.software on mission publish; progress fields updated by the Custodian from telemetry. current_mission_id text NULL, current_flight_state text NULL, -- PLANNED | ACTIVE | COMPLETED | ABORTED current_flight_window_start timestamptz NULL, current_flight_window_end timestamptz NULL, current_waypoint_index int4 NULL, current_waypoint_total int4 NULL, current_progress_pct int4 NULL, -- Current OI (v1.9) — the currently active Operational Intent for this drone. -- Written by the Flink OI Automation job on every generation/regeneration. current_oi_id text NULL, current_oi_state text NULL, -- DRAFT | SUBMITTED | ACCEPTED | ACTIVE | COMPLETED | REJECTED current_oi_source text NULL, -- AUTOMATED | MANUAL | MISSION_PUBLISH current_oi_window_start timestamptz NULL, current_oi_window_end timestamptz NULL, oi_last_regenerated_at timestamptz NULL, oi_next_regeneration_at timestamptz NULL, -- Live state (Custodian writes these from GCS plugin telemetry) position_lat float8 NULL, position_lon float8 NULL, position_alt float8 NULL, flight_mode text NULL, armed bool NULL, battery_remaining_pct int4 NULL, link_rssi_dbm int4 NULL, adapter_connected bool DEFAULT false NOT NULL, last_telemetry_at timestamptz NULL, -- Cross-references aadms_id text NULL, -- cross-tenant identity reference -- Timestamps created_at timestamptz DEFAULT now() NOT NULL, updated_at timestamptz DEFAULT now() NOT NULL, retired_at timestamptz NULL, CONSTRAINT drones_pkey PRIMARY KEY (tenant_id, serial_number), CONSTRAINT drones_dlb_guid_fk FOREIGN KEY (dlb_guid) REFERENCES drones.dlb_drones(guid), CONSTRAINT drones_dlb_guid_unique UNIQUE (dlb_guid) ); CREATE INDEX idx_drones_serial ON drones.drones USING btree (serial_number);
drones.v_dtwin)Read-only view that joins drones.drones with drones.dlb_drones on the dlb_guid foreign key. This is the authoritative dTwin data record — the single consistent view consumers should use when they need both RTM state and DLB identity together. The Custodian APIs serve responses that reflect this aggregated record.
-- drones.v_dtwin · /tenant/rtm-app-db/schema/v_dtwin.sql -- dTwin view: joins drones.drones (RTM state) with drones.dlb_drones (DLB identity). -- Use this view for any consumer that needs the full drone record. CREATE OR REPLACE VIEW drones.v_dtwin AS SELECT -- Core identity d.tenant_id, d.serial_number, d.dlb_guid, -- DLB identity (from dlb_drones; renamed for clarity) dlb.name AS drone_name, dlb.brand, dlb.model, dlb.drone_type, dlb.status, dlb.color, dlb.identification_number AS "TC_Registration", -- TC / regulatory registration dlb.inventory_number AS "Callsign", -- operator-assigned ADS-B callsign dlb.hardware_version, dlb.firmware_version, dlb.max_horizontal_speed, dlb.max_vertical_speed, dlb.max_flight_time, dlb.payload_capacity, dlb.weight, dlb.propulsion_type, -- Custodian configuration (RTM-managed) d.adapter_type, d.adapter_config, d.publish_rate_hz, -- OI defaults & FLYplan automation d.oi_automation_enabled, d.horizontal_speed_ms, d.vertical_speed_ms, d.fv_buffer_m, d.cv_buffer_m, d.oi_automation_square_m, d.oi_automation_radius_m, d.oi_automation_amend_min_trigger_m, d.oi_automation_amend_max_alt_agl_m, d.oi_automation_amend_enabled, d.waypoint_buffer_m, -- Pilot & separation defaults (v1.7) d.default_pilot_name, d.default_pilot_fs_guid, d.nmac_h_crewed_m, d.nmac_v_crewed_m, d.nmac_h_uncrewed_m, d.nmac_v_uncrewed_m, d.swc_h_crewed_m, d.swc_v_crewed_m, d.swc_h_uncrewed_m, d.swc_v_uncrewed_m, d.eval_distance_m, -- ADS-B provisioning d.adsb_in_enabled, d.adsb_out_enabled, d.adsb_surrogate_enabled, d.tisb_rebroadcast_enabled, -- Conspicuity / ADS-B detail (v1.13) d.ec_adsb_hex, d.adsb_out_device, d.adsb_hex_mode, d.surveillance_mode, d.daa_enabled, d.broadcast_binding, d.networkid_streaming_enabled, -- Observed ADS-B state (v1.13) d.adsb_observed_broadcasting, d.adsb_observed_last_icao_hex, d.adsb_observed_last_seen_by_cifib_at, d.ownership_flag_enabled, -- Data source monitoring (v1.7) — see §3.6 d.data_source_mechanism, d.data_source_rate_hz, d.data_source_latency_ms_p50, d.data_source_latency_ms_p95, d.data_source_status, d.data_source_max_latency_ms, d.data_source_last_seen_at, -- Current flight & current OI (v1.9) d.current_mission_id, d.current_flight_state, d.current_flight_window_start, d.current_flight_window_end, d.current_waypoint_index, d.current_waypoint_total, d.current_progress_pct, d.current_oi_id, d.current_oi_state, d.current_oi_source, d.current_oi_window_start, d.current_oi_window_end, d.oi_last_regenerated_at, d.oi_next_regeneration_at, -- Live state d.position_lat, d.position_lon, d.position_alt, d.flight_mode, d.armed, d.battery_remaining_pct, d.link_rssi_dbm, d.adapter_connected, d.last_telemetry_at, -- Cross-references & timestamps d.aadms_id, d.created_at, d.updated_at, d.retired_at FROM drones.drones d JOIN drones.dlb_drones dlb ON dlb.guid = d.dlb_guid;
aadms.drones)Cross-tenant drone lookup table used by the ADS-B routing pipeline. In the current architecture this is a materialized view that aggregates drones.drones (joined to dlb_drones for callsign) from every tenant schema into a single flat surface. ADS-B records arrive carrying only callsign; AADMS resolves callsign → (tenant_id, serial_number) so the record can be routed to the correct tenant pipeline. Refreshed after each Import sync run.
-- aadms.drones · cross-tenant drone lookup -- Materialized view aggregating all tenant drones.drones rows (joined to dlb_drones for callsign). -- Refreshed after every Import sync. Used by ADS-B routing to resolve callsign → (tenant, serial). CREATE SCHEMA IF NOT EXISTS aadms; CREATE MATERIALIZED VIEW aadms.drones AS -- One UNION ALL block per tenant schema; extend as tenants are added. SELECT d.tenant_id, d.serial_number, d.dlb_guid AS guid, dlb.inventory_number AS callsign, d.aadms_id AS id FROM tenant_airmarket.drones.drones d JOIN tenant_airmarket.drones.dlb_drones dlb ON dlb.guid = d.dlb_guid WHERE d.retired_at IS NULL UNION ALL SELECT d.tenant_id, d.serial_number, d.dlb_guid AS guid, dlb.inventory_number AS callsign, d.aadms_id AS id FROM tenant_sait.drones.drones d JOIN tenant_sait.drones.dlb_drones dlb ON dlb.guid = d.dlb_guid WHERE d.retired_at IS NULL -- ... repeat for each tenant ... ; CREATE UNIQUE INDEX idx_aadms_drones_callsign ON aadms.drones (callsign); CREATE INDEX idx_aadms_drones_serial ON aadms.drones (serial_number);
REFRESH MATERIALIZED VIEW CONCURRENTLY aadms.drones at the end of each Import sync run. The CONCURRENTLY option keeps the view queryable during refresh (requires the unique index on callsign). DEV and PROD use separate AADMS schemas so refreshes are independent.
fv_buffer_m/cv_buffer_m on drones.drones are configuration — a single horizontal buffer distance used to compute a volume. They are not the volume itself. Every time the Flink OI Automation job generates or regenerates an OI, it computes three distinct geometries (Flight Volume, Contingency Volume, Submitted Volume) from those buffers plus the predicted trajectory.
drones.oi_geometry table. Earlier versions of this doc (v1.11–v1.12) specified a dedicated drones.oi_geometry table for these three geometries. That table was never built. The three geometries for current and upcoming flights are instead persisted in the existing flights_binding table — the same table already used to bind drones to their current/upcoming flights, outside the drones schema. This doc does not restate flights_binding's own column definitions; see its owning schema/doc for the full DDL. Wherever this doc previously said “persisted in drones.oi_geometry,” read “persisted in flights_binding” instead — the conceptual point (all three geometries are stored, not just the buffer inputs; submitted_volume is stored separately rather than recomputed) still holds, only the storage location changed.
drones.daa_rules)(v1.13) One row per Detect-and-Avoid rule per drone — the ordered rule set the mockup's Conspicuity/DAA panel edits as daa_rules[]. Rules are evaluated in sort_order; each can be individually enabled/disabled without deleting it.
-- drones.daa_rules · one row per DAA rule per drone -- Read/written by the Custodian API's DAA endpoints; evaluated by the DAA service at runtime. CREATE TABLE drones.daa_rules ( tenant_id text NOT NULL, serial_number text NOT NULL, rule_id text NOT NULL, -- e.g. R1, R2, R3 condition_text text NOT NULL, -- human-readable trigger condition, e.g. "Target within NMAC radius" action_text text NOT NULL, -- action taken when triggered, e.g. "RTH — Immediate" priority text NOT NULL, -- critical | high | medium | low enabled bool NOT NULL DEFAULT true, sort_order int4 NOT NULL DEFAULT 0, -- evaluation / display order PRIMARY KEY (tenant_id, serial_number, rule_id), FOREIGN KEY (tenant_id, serial_number) REFERENCES drones.drones(tenant_id, serial_number) );
drones.dlb_drones and drones.drones work togetherdrones.dlb_drones and drones.drones are not alternatives — they are two complementary layers of the same pipeline, each with a distinct owner and purpose. drones.dlb_drones is the primary store for all raw DLB API data, written by the Import process and never touched by RTM. drones.drones holds only RTM state — configuration, live telemetry, and the dlb_guid foreign key that links it to dlb_drones. DLB identity data is never duplicated into drones.drones; consumers that need the full picture use the v_dtwin view.
Three distinct processes write to these tables at different times. Each process owns specific columns and must not write to columns owned by another. Breaking that boundary — for example, the Import process overwriting operator OI configuration, or the Custodian writing into dlb_drones — corrupts the data contract.
drones.dlb_drones — the single authoritative copy of DLB data. All columns are overwritten on every sync run. Column names are preserved exactly as DLB delivers them. On first insert, also creates the corresponding row in drones.drones with dlb_guid and seeds the two speed defaults.
position_lat/lon/alt, flight_mode, armed, battery_remaining_pct, adapter_connected, last_telemetry_at. Neither the Import process nor operators write these columns directly.
v_dtwin view joins all three surfaces into a single consistent dTwin record. The Custodian API responses, frontend UI, and MCP tools all read from this view when the full drone record is needed.
dlb_drones into drones.dronesdlb_drones column |
drones.drones column |
Sync rule | Notes |
|---|---|---|---|
guid |
dlb_guid |
Written on first INSERT | Foreign key linking drones.drones to its source row in drones.dlb_drones. Immutable after first insert — the link never changes. |
serial_number |
serial_number |
Written on first INSERT | Forms the composite PK (tenant_id, serial_number). Immutable in practice after first insert. |
max_horizontal_speed (TEXT) |
horizontal_speed_ms (FLOAT) |
Seeded on first INSERT only | Cast TEXT → FLOAT during import. Subsequent sync runs skip this column if the row already exists — preserving any value an operator has set via PUT /drones/{serial}/oi-defaults. |
max_vertical_speed (TEXT) |
vertical_speed_ms (FLOAT) |
Seeded on first INSERT only | Same seeding rule as horizontal speed. Operator overrides are preserved across syncs. |
| All other dlb_drones columns (name, brand, model, callsign, TC reg, status, hardware specs, etc.) | Not copied | Remain exclusively in drones.dlb_drones. Accessible in full via the v_dtwin view (JOIN on dlb_guid). Not duplicated into drones.drones. |
|
RTM-managed columns (adapter_type, oi_automation_enabled, ADS-B flags, etc.) |
Import never writes | Owned exclusively by the Custodian APIs (process 2). The Import process must exclude these from its upsert so operator configuration is never silently reset. | |
Live-state columns (position_lat/lon/alt, flight_mode, armed, etc.) |
Import never writes | Owned exclusively by the Custodian’s telemetry ingestion path (process 3). Set to NULL / defaults until the first GCS Plugin frame arrives. |
|
INSERT … ON CONFLICT (tenant_id, serial_number) DO NOTHING to create the drones.drones row with dlb_guid, serial_number, and the two speed seeds. All subsequent sync runs update only drones.dlb_drones — they do not touch drones.drones at all. RTM-managed and live-state columns are never in scope for the Import process.
| Category | Primary store | Writer | Examples |
|---|---|---|---|
| DLB identity | drones.dlb_drones (verbatim DLB copy) |
Import script (Steve Smith) — RTM side cannot modify | name, brand, model, drone_type, status, identification_number, inventory_number — exposed via v_dtwin with friendly aliases |
| RTM-managed | drones.drones |
Custodian APIs (operator UI, Custodian itself, MCP agents) | adapter_type, publish_rate_hz, oi_automation_enabled, oi_automation_square_m, oi_automation_radius_m, oi_automation_amend_min_trigger_m, waypoint_buffer_m, fv_buffer_m, cv_buffer_m, default_pilot_name, default_pilot_fs_guid, nmac_h_crewed_m/nmac_v_crewed_m/nmac_h_uncrewed_m/nmac_v_uncrewed_m, swc_h_crewed_m/swc_v_crewed_m/swc_h_uncrewed_m/swc_v_uncrewed_m, eval_distance_m, adsb_surrogate_enabled, tisb_rebroadcast_enabled, ownership_flag_enabled, current_mission_id, current_flight_state, current_oi_id, current_oi_state |
| Live state | drones.drones |
Custodian (server-side) writing as it normalizes plugin POSTs | position_lat, position_lon, position_alt, flight_mode, armed, battery_remaining_pct, adapter_connected, last_telemetry_at |
drones.dlb_drones is overwritten on every sync run. drones.drones is only touched by the Import process on first insert (writing dlb_guid, serial_number, and seeding the two speed defaults). RTM-managed and live-state columns are never in scope for the Import process.
The dTwin is the conceptual digital twin of one aircraft — the canonical answer to “what does FLYsafe.live believe is true about this drone right now?” It is the v_dtwin record for that drone: the join of drones.drones (RTM configuration and live state) with drones.dlb_drones (DLB identity). Consumers don’t need to know which table backs which field; they call the API and get a coherent record sourced from the view.
{
"identity": {
"serial_number": "1581F5BBB1F2A",
"tc_registration": "C-FAIRTL69",
"callsign": "AIRTL69",
"tenant": "airmarket",
"adapter_type": "mavlink"
},
"position": { "lat": 53.5461, "lon": -113.4938, "alt_msl_m": 720.4 },
"flight_mode": "AUTO",
"armed": true,
"battery": { "remaining_pct": 68 },
"adsb": {
"in_enabled": true,
"out_enabled": true,
"surrogate_enabled": false,
"ownership_flagged": true // see §9
},
"pilot": { // v1.7 — default pilot binding
"default_name": "Jane Chen",
"default_fs_guid": "pilot-8f3a2e19-4c1a-4e6b-9c17-2b9e5d3f7a0c"
},
"oi": {
"automation_enabled": true, // pilot-granted; see §5A (v1.10)
"default_distance_m": 300,
"waypoint_buffer_m": 50.0 // v1.10 — radius: drone "reached" waypoint when within
},
"volumes": { // v1.8; horizontal-only as of v1.12 — see current_oi.geometry_ref for the actual computed polygons
"flight_volume": { "buffer_m": 56.0 },
"contingency_volume": { "buffer_m": 150.0 }
},
"separation": { // v1.7; split into crewed/uncrewed × horizontal/vertical in v1.12
"nmac_crewed_m": { "h": 150.0, "v": 30.0 },
"nmac_uncrewed_m": { "h": 15.0, "v": 8.0 },
"swc_crewed_m": { "h": 1500.0, "v": 150.0 },
"swc_uncrewed_m": { "h": 150.0, "v": 30.0 },
"eval_distance_m": 1852.0
},
"data_source": { // v1.7 — see §3.6
"mechanism": "mp_plugin",
"rate_hz": 4.0,
"latency_ms_p50": 180,
"latency_ms_p95": 420,
"max_latency_ms": 2000,
"status": "HEALTHY",
"last_seen_at": "2026-06-16T18:42:18.315Z"
},
"current_flight": { // v1.9 — the mission this drone is executing right now
"mission_id": "mission-airmarket-2026061618420",
"state": "ACTIVE", // PLANNED | ACTIVE | COMPLETED | ABORTED
"window": { "start": "2026-06-16T18:30:00Z", "end": "2026-06-16T19:15:00Z" },
"waypoints": { "current_index": 5, "total": 12 },
"progress_pct": 42
},
"current_oi": { // v1.9 — the active Operational Intent for this drone
"oi_id": "oi-airmarket-2026061618421",
"state": "ACTIVE", // DRAFT | SUBMITTED | ACCEPTED | ACTIVE | COMPLETED | REJECTED
"source": "AUTOMATED", // AUTOMATED | MANUAL | MISSION_PUBLISH
"last_regenerated_at": "2026-06-16T18:41:15Z",
"next_regeneration_at": "2026-06-16T18:41:45Z",
"geometry_ref": { // v1.11 — three distinct stored geometries, not one
"flight_volume": "/api/v1/drones/1581F5BBB1F2A/oi/current/flight-volume",
"contingency_volume": "/api/v1/drones/1581F5BBB1F2A/oi/current/contingency-volume",
"submitted_volume": "/api/v1/drones/1581F5BBB1F2A/oi/current" // FV + CV combined; what was sent to FLYrtm DSS
}
},
"adapter_health": {
"connected": true,
"last_publish_ago_ms": 247
},
"timestamps": {
"updated_at": "2026-06-16T18:42:18.523Z",
"source_ts": "2026-06-16T18:42:18.491Z"
}
}
↑ Returned by GET /drones/{serial}. The Custodian's REST API serves this from the tenant.drones row + latest telemetry. There is no longer a canonical “dTwin payload schema” separate from the API response — the response is the schema. Cheaper subset endpoints (/state, /current-flight, /current-oi, /data-source) exist for consumers that don’t want the full payload every time.
This is the catalog of API operations the Aircraft Custodian exposes. The same APIs are consumed by:
tenant.drones.All endpoints are tenant-scoped (the tenant is resolved from the JWT). The base path is /api/v1. Verbs use REST conventions: GET reads, POST creates/triggers, PUT updates, DELETE removes. Where a notification stream is useful, a WebSocket endpoint is offered as a parallel option to polling.
drones.drones. This gives per-consumer field isolation and stable contracts as the schema evolves.
Operations supporting the FLYplan workflow — automated OI creation, per-drone OI defaults, OI automation toggling, FV/CV volume buffers, and the waypoint-arrival buffer.
current_flight_state flips from PLANNED to ACTIVE on armed+airborne detection, the Flink OI Automation job regenerates the OI as trajectory changes warrant, and current_oi_* is refreshed on every acceptance from DSS. The pilot can revoke authority at any time by flipping the toggle back to off, at which point the Custodian holds the last-known state and does not auto-regenerate.
fv_buffer_m / cv_buffer_m on drones.drones are per-drone defaults for these buffers and are pulled into every auto-generated OI unless the mission overrides them at publication. FV/CV carry no vertical component — vertical separation from other traffic is the job of the NMAC/SWC crewed/uncrewed minima below, not FV/CV. These fields supersede the flat flight_volume_m / contingency_vol_m scalars used through v1.7, and the v1.8–v1.11 lateral+vertical split.
flights_binding in v1.13). FV and CV are not just configuration inputs — each OI generation produces three distinct geometries, and all three are persisted in flights_binding and exposed via the Custodian API: the Flight Volume polygon, the Contingency Volume polygon, and the Submitted Volume — the single specific polygon that is the combination of FV and CV, and the one actually transmitted to the FLYrtm DSS. GET /drones/{serial}/oi/current returns the Submitted Volume; the two component volumes each have their own endpoint below.
| Endpoint | Purpose | Primary Caller |
|---|---|---|
| GET/drones/{serial}/oi-automation | Read the OI Automation status for a drone (enabled, defaults, last regeneration time). | Frontend · MCP |
| PUT/drones/{serial}/oi-automation | Enable or disable OI Automation for this drone. Body: { enabled: bool }. This is the pilot-authority toggle (v1.10) — when enabled, the Custodian and Flink OI Automation job manage the drone’s OI lifecycle autonomously. | Frontend · MCP |
| PUT/drones/{serial}/oi-defaults | Set OI default parameters: horizontal_speed_ms, vertical_speed_ms, oi_automation_square_m, oi_automation_radius_m, oi_automation_amend_min_trigger_m. Partial updates supported; ranges enforced server-side. | Frontend |
| GET/drones/{serial}/oi-defaults | Read the current OI defaults. | Frontend · MCP · Custodian (boot) |
| GET/drones/{serial}/volumes | Read the drone’s FV and CV buffer defaults (config, not geometry) — horizontal only: { flight_volume: { buffer_m }, contingency_volume: { buffer_m } }. | Frontend · MCP · Flink · Custodian (boot) |
| PUT/drones/{serial}/volumes | Set FV and/or CV buffer defaults. Body accepts either or both of fv_buffer_m / cv_buffer_m. Server-side clamp enforces fv_buffer_m ≤ cv_buffer_m, so CV always contains FV. | Frontend |
| GET/drones/{serial}/waypoint-buffer | Read the waypoint-arrival buffer in metres (waypoint_buffer_m, default 50 m). Flight-monitoring jobs treat the aircraft as having “reached” a waypoint once it enters this radius. | Frontend · MCP · Flink |
| PUT/drones/{serial}/waypoint-buffer | Set the waypoint-arrival buffer. Body: { waypoint_buffer_m: number }. Server-side clamp: 5 m ≤ value ≤ 500 m. | Frontend |
| POST/drones/{serial}/oi/regenerate | Force-regenerate the OI now (skip waiting for the next trajectory tick). Useful for testing or after a defaults change. (v1.13) Backend-only — not surfaced as a manual action in the RTM Map GUI; available to MCP/ops tooling. | MCP |
| GET/drones/{serial}/oi/current | Fetch the Submitted Volume — the GeoJSON polygon that is the combination of FV and CV and was actually sent to FLYrtm DSS, with active mission reference. (v1.11: previously described ambiguously as “the OI geometry”; now explicitly the combined/submitted polygon.) | Frontend · Flink |
| GET/drones/{serial}/oi/current/flight-volume | (v1.11) Fetch the Flight Volume geometry alone — the trajectory expanded by the FV buffers, as its own GeoJSON polygon. | Frontend (Map) · Compliance AI |
| GET/drones/{serial}/oi/current/contingency-volume | (v1.11) Fetch the Contingency Volume geometry alone — the trajectory expanded by the CV buffers, as its own GeoJSON polygon. Always contains the Flight Volume. | Frontend (Map) · Compliance AI |
| GET/drones/{serial}/current-oi | Fetch the current OI summary: { oi_id, state, source, window, last_regenerated_at, next_regeneration_at, geometry_ref }. Cheap — no polygons in the payload; geometry_ref links to the three geometry endpoints above. The same block appears inline in GET /drones/{serial}. | Frontend · MCP · Compliance AI |
| WS/drones/{serial}/current-oi/watch | Subscribe to current-OI state transitions (SUBMITTED → ACCEPTED → ACTIVE → COMPLETED) and regeneration events. Frontend uses this to update the OI badge without polling. | Frontend · OCC |
| GET/drones/{serial}/oi/history | List previous OI generations for this drone within a time range, including all three stored geometries per generation. For audit and FLYplan post-flight review. | Compliance AI · Evidence |
Three new OI geometry parameters have been added to the drone configuration. These are stored in drones.drones and exposed through the OI Automation API endpoints.
| Field | Default | Valid range | Description |
|---|---|---|---|
oi_automation_square_m |
700 m | 350 – 1400 m | OI square side length used in the FLYplan geometry calculation. |
oi_automation_radius_m |
400 m | 200 – 800 m | OI radius used in the FLYplan geometry calculation. |
oi_automation_amend_min_trigger_m |
100 m | 50 – 200 m | Minimum aircraft displacement (m) required to trigger an OI amendment. |
defaults block now includes all three fields.enabled: true — returns 422 if any of the three fields is null; ensure they are set before enabling OI automation.ConfigSaveBody accepts the three new fields; same null-before-enable constraint applies when oi_automation_enabled: true is sent.Per-drone ADS-B configuration. v1 uses a single surrogate boolean; the schema notes anticipate splitting into in/out/runtime fields in a future revision (see DM-DI in §10 Open Questions).
| Endpoint | Purpose | Primary Caller |
|---|---|---|
| GET/drones/{serial}/adsb | Read ADS-B configuration block: in_enabled, out_enabled, surrogate_enabled. | Frontend · MCP · Custodian |
| PUT/drones/{serial}/adsb | Update the full ADS-B configuration block in one call. Body: { in_enabled, out_enabled, surrogate_enabled }. | Frontend |
| PUT/drones/{serial}/adsb-surrogate | Toggle just the ADS-B Surrogate flag. Convenience endpoint; equivalent to a PATCH on the block. | Frontend · MCP |
| PUT/drones/{serial}/tisb-rebroadcast | Toggle the TIS-B Rebroadcast flag (tisb_rebroadcast_enabled). Body: { enabled: bool }. When enabled, the Custodian rebroadcasts this aircraft’s own telemetry into the TIS-B feed so ground-based ADS-B receivers can observe it. | Frontend · MCP |
| GET/drones/{serial}/adsb/status | Read the live ADS-B status from telemetry: is the transponder actually broadcasting? Last observed ICAO hex? Last seen by CIFIB? | Frontend · Compliance AI |
The Ownership flag is the operator’s explicit surveillance intent for one of their drones. When set, surveillance services (Flight_Live, §9) tag inbound records of this aircraft as own-aircraft so consumers can apply their per-purpose filtering.
| Endpoint | Purpose | Primary Caller |
|---|---|---|
| GET/drones/{serial}/ownership | Read the current ownership flag state. | Frontend · Flight_Live |
| PUT/drones/{serial}/ownership | Set the ownership flag. Body: { enabled: bool, reason?: string }. The reason field supports audit. | Frontend · MCP (permission-scoped) |
The configuration block the Custodian itself fetches at boot. Carries adapter type, GCS plugin endpoints, broker addresses, publish rate, and OI defaults bundled as one document.
| Endpoint | Purpose | Primary Caller |
|---|---|---|
| GET/drones/{serial}/config | Custodian boot config — the full configuration block the Custodian needs to instantiate. Returns adapter type, publish rate, ADS-B block, OI defaults, ownership flag, etc. | Custodian (on boot) |
| GET/drones/{serial}/config?since={ts} | Returns config only if changed since the given timestamp. Supports cheap polling from the Custodian for change detection. | Custodian (watch loop) |
| WS/drones/{serial}/config/watch | WebSocket subscription that pushes config changes as they happen. Optional — polling is the simpler fallback. | Custodian (optional) |
| PUT/drones/{serial}/adapter-config | Update adapter-specific connection block (e.g. MAVLink host/port, DJI FH2 endpoint). | Frontend |
| PUT/drones/{serial}/publish-rate | Change the telemetry publish rate (Hz). Custodian hot-reloads. | Frontend · MCP |
State-read endpoints for consumers, and the ingestion endpoint the GCS Plugins POST to.
| Endpoint | Purpose | Primary Caller |
|---|---|---|
| GET/drones | List drones for the tenant. Supports filters (active flight, callsign substring, model). Paginated. | Frontend · MCP |
| GET/drones/{serial} | Full drone record — identity, config, current live state. The canonical dTwin view. | All consumers |
| GET/drones/{serial}/state | Just the live state subset (position, mode, battery, link, adapter_health) without configuration noise. Cheaper for high-frequency consumers. | Map (live layer) |
| GET/drones/{serial}/current-flight | Fetch the current-flight block: { mission_id, state, window, waypoints, progress_pct }. Reflects the mission the drone is executing right now (v1.9). The same block appears inline in GET /drones/{serial}. | Frontend · MCP · Compliance AI |
| WS/drones/{serial}/current-flight/watch | Subscribe to flight-state transitions (PLANNED → ACTIVE → COMPLETED) and waypoint progress updates. Frontend uses this to drive the mission-progress bar. | Frontend |
| GET/drones/{serial}/telemetry | Recent telemetry timeseries (positions, modes, battery) within a time range. Backed by InfluxDB. | Map (trails) · Evidence |
| WS/drones/{serial}/state/watch | Subscribe to live state updates for one drone. Replaces the v1.5 MQTT topic subscription for direct UI use cases. | Frontend (map live layer) |
| POST/drones/{serial}/telemetry | Ingestion endpoint. The GCS Plugin POSTs normalized telemetry frames here. The Custodian receives, normalizes again if needed, writes the live-state columns of tenant.drones, and appends to InfluxDB. | GCS Plugins (Mission Planner, Chrome) |
| POST/drones/{serial}/alerts | Plugin-emitted alerts (mode change, RTL triggered, geofence breach, low battery). Routed to OCC and stored in evidence. | GCS Plugins · OCC AI |
Binds an active mission to a drone for the duration of an OI window. This is how callsign ↔ serial association becomes authoritative (used by Flink’s ADS-B network_id resolution).
| Endpoint | Purpose | Primary Caller |
|---|---|---|
| GET/drones/{serial}/mission | Read the active mission binding (mission_id, callsign, OI window, pilot_id). | Frontend · Flink · Compliance AI |
| POST/drones/{serial}/mission | Create a mission binding. Body includes mission_id, callsign, OI window start/end, pilot_id. | FLYsafe.software (on mission publish) |
| DELETE/drones/{serial}/mission | Clear the active mission binding (after flight completion or cancellation). | FLYsafe.software · Frontend |
Identity registration in Keycloak and cross-tenant publication into AADMS. Run by the Import process and by the Custodian on first boot.
| Endpoint | Purpose | Primary Caller |
|---|---|---|
| POST/drones/{serial}/identity/keycloak | Register the drone as a user in Keycloak. ID format <tenant>-<serial>. Idempotent. | Import process · Custodian (boot) |
| POST/drones/{serial}/identity/aadms | Publish the drone’s (tenant, serial, callsign) tuple to AADMS for cross-tenant routing of inbound ADS-B records. | Import process · Custodian |
| GET/drones/{serial}/identity | Inspect identity state — Keycloak client ID, AADMS ID, registration timestamps. | Frontend (admin view) |
Health probes for the Custodian, and lifecycle events (drone retired, archived, restored).
| Endpoint | Purpose | Primary Caller |
|---|---|---|
| GET/drones/{serial}/health | Health of the Custodian for this drone: adapter_connected, last_telemetry_at, plugin_session_id, plugin_version. | OCC · Monitoring · Compliance AI |
| POST/drones/{serial}/retire | Mark drone as retired. Stops accepting telemetry, clears live state, archives the row. | Frontend (admin) |
| POST/drones/{serial}/restore | Restore a retired drone (re-enable telemetry acceptance). | Frontend (admin) |
Per-drone default pilot binding and conflict-evaluation minima. These become the defaults every mission and OI inherit unless the operator overrides at mission publication.
| Endpoint | Purpose | Primary Caller |
|---|---|---|
| GET/drones/{serial}/default-pilot | Read the default pilot for this drone: { name, fs_guid }. Used as the pre-filled pilot when a new mission is created without explicit selection. | Frontend · MCP |
| PUT/drones/{serial}/default-pilot | Set the default pilot. Body: { name: string, fs_guid: string }. The fs_guid is validated against FLYsafe.software’s pilot registry before the write is committed. | Frontend |
| DELETE/drones/{serial}/default-pilot | Clear the default pilot binding for this drone. | Frontend |
| GET/drones/{serial}/separation-defaults | Read the per-drone NMAC, SWC, and EVAL distances in metres, split by crewed/uncrewed traffic and horizontal/vertical (v1.12): { nmac_crewed_m: {h,v}, nmac_uncrewed_m: {h,v}, swc_crewed_m: {h,v}, swc_uncrewed_m: {h,v}, eval_distance_m }. | Frontend · MCP · Flink (OI Automation) |
| PUT/drones/{serial}/separation-defaults | Set NMAC, SWC, and EVAL distances. Body accepts any subset of the eight NMAC/SWC fields plus eval_distance_m. Server-side validation enforces nmac < swc < eval for each matching horizontal/vertical and crewed/uncrewed pair, and clamps to sane bounds. | Frontend |
Read-only surface for the vigilance state the Custodian maintains for each drone. See §3.6 for the concept; these endpoints expose it to consumers.
| Endpoint | Purpose | Primary Caller |
|---|---|---|
| GET/drones/{serial}/data-source | Read the summary source-health block: { mechanism, rate_hz, latency_ms_p50, latency_ms_p95, max_latency_ms, status, last_seen_at }. | Frontend (Map) · OCC · MCP |
| PUT/drones/{serial}/data-source/envelope | Set the expected envelope: { max_latency_ms }. Rate expectations are derived from publish_rate_hz already on the drone. | Frontend (admin) |
| WS/drones/{serial}/data-source/watch | WebSocket that pushes status transitions (HEALTHY → DEGRADED, DEGRADED → STALE, etc.) as they happen. Optional; polling GET /drones/{serial}/data-source is the fallback. | OCC AI (live) |
| GET/drones/{serial}/data-source/history | Time-range query of status transitions and rate/latency samples for post-flight review. Backed by InfluxDB. Feeds CAR 922 evidence — connectivity gaps are part of the operational record. | Compliance AI · Evidence |
The MCP server wraps a curated subset of the above as AI-callable tools. The principle is read freely, write narrowly: agents can read drone state, OI status, ownership state, identity, and health without restriction; writes are scoped to operationally-safe toggles only, and require explicit per-tenant agent permissions.
| MCP tool name | Maps to | Read/Write | Notes |
|---|---|---|---|
drone.list | GET /drones | R | List drones in scope. |
drone.get | GET /drones/{serial} | R | Full record. |
drone.get_state | GET /drones/{serial}/state | R | Live state only. |
drone.get_telemetry | GET /drones/{serial}/telemetry | R | Time-range query. |
drone.set_oi_automation | PUT /drones/{serial}/oi-automation | W | Operationally safe. |
drone.set_oi_distance | PUT /drones/{serial}/oi-defaults | W | Numeric clamp applied. |
drone.set_adsb_surrogate | PUT /drones/{serial}/adsb-surrogate | W | Operationally safe. |
drone.set_ownership | PUT /drones/{serial}/ownership | W | Permission-scoped. |
drone.regenerate_oi | POST /drones/{serial}/oi/regenerate | W | Operationally safe. |
drone.get_health | GET /drones/{serial}/health | R | Used heavily by OCC AI. |
drone.get_default_pilot | GET /drones/{serial}/default-pilot | R | Prefill for mission creation. (v1.7) |
drone.set_default_pilot | PUT /drones/{serial}/default-pilot | W | Permission-scoped; validates against FLYsafe.software. (v1.7) |
drone.get_separation_defaults | GET /drones/{serial}/separation-defaults | R | NMAC / SWC (crewed/uncrewed × horizontal/vertical) / EVAL in metres. (v1.7; split v1.12) |
drone.set_separation_defaults | PUT /drones/{serial}/separation-defaults | W | Server-side clamp enforces nmac < swc < eval per h/v & crewed/uncrewed pair. (v1.7; split v1.12) |
drone.get_data_source | GET /drones/{serial}/data-source | R | Source-vigilance summary. High value for OCC AI. (v1.7) |
drone.get_data_source_history | GET /drones/{serial}/data-source/history | R | Post-flight connectivity review; feeds Compliance AI. (v1.7) |
drone.get_volumes | GET /drones/{serial}/volumes | R | FV and CV buffer defaults. (v1.8) |
drone.set_volumes | PUT /drones/{serial}/volumes | W | Server-side clamp enforces CV ≥ FV in both dimensions. (v1.8) |
drone.get_current_flight | GET /drones/{serial}/current-flight | R | Current mission state; high value for OCC AI and Compliance AI. (v1.9) |
drone.get_current_oi | GET /drones/{serial}/current-oi | R | Current OI summary (state, window, regeneration times, geometry_ref). (v1.9) |
drone.get_submitted_volume | GET /drones/{serial}/oi/current | R | The polygon actually sent to DSS (FV + CV combined). Heavy payload. (v1.11) |
drone.get_flight_volume_geometry | GET /drones/{serial}/oi/current/flight-volume | R | FV polygon alone. Heavy payload; used by Compliance AI. (v1.11) |
drone.get_contingency_volume_geometry | GET /drones/{serial}/oi/current/contingency-volume | R | CV polygon alone. Heavy payload; used by Compliance AI. (v1.11) |
drone.get_waypoint_buffer | GET /drones/{serial}/waypoint-buffer | R | Radius in metres for waypoint arrival detection. (v1.10) |
drone.set_waypoint_buffer | PUT /drones/{serial}/waypoint-buffer | W | Server-side clamp: 5 m ≤ value ≤ 500 m. (v1.10) |
Operations not on this list (retire, identity registration, mission binding writes, adapter-config changes, retire/restore) are deliberately not exposed via MCP. They can be added incrementally as the agent permission model matures.
FLYsafe.live carries a drone’s identity across four systems. Each tier has a distinct purpose; mixing them up breaks the binding pipeline.
| System | Identity Key | Holds | Purpose |
|---|---|---|---|
| FLYsafe.software DLB | dlb_guid |
Authoritative drone inventory — brand, model, serial, TC registration, core RID fields | System of record for inventory |
| tenant.drones + dlb_drones → v_dtwin (rtm-app-db) | (tenant_id, serial_number) |
drones.drones: RTM-managed config + live state + dlb_guid FK. drones.dlb_drones: verbatim DLB identity. v_dtwin: the JOIN that forms the complete dTwin record. |
The dTwin lives here (v_dtwin view) |
| Keycloak.θ.Drone | <tenant>-<serial> |
Drone-as-user authentication — credentials, group membership, service access | Authentication only |
| AADMS | (tenant, serial) + callsign |
Cross-tenant identity lookup — callsign → tenant routing for inbound ADS-B records that don’t carry a tenant | Cross-tenant routing |
Serial number is the only one used as a binding key across the system. Callsign is set per-mission and used only for ADS-B inbound resolution via AADMS. ICAO hex is dynamic per session (uAvionix-aligned ADDRt) and is never bound to. TC registration is regulatory display.
The FLYplan process is the automated creation and maintenance of Operational Intents (OIs) for live flights. The Custodian APIs (§5A) are the surface that operators and AI agents use to turn this on, set its parameters, and observe its output.
| Step | What happens | API call(s) |
|---|---|---|
| 1. Onboard the drone | Operator sets per-drone OI defaults (distance, speed limits), volume defaults (FV buffer, CV buffer — horizontal only), separation defaults (NMAC / SWC, each crewed/uncrewed × horizontal/vertical, plus EVAL), and default pilot (name + FLYsafe.software GUID). These become the baseline every mission and auto-generated OI inherits. | PUT /drones/{serial}/oi-defaultsPUT /drones/{serial}/volumesPUT /drones/{serial}/separation-defaultsPUT /drones/{serial}/default-pilot |
| 2. Pilot grants OI Automation authority | Pilot flips the per-drone OI Automation toggle on the Inventory page of the RTM Map (or an AI agent does it on the pilot’s behalf with scoped permission). This is the explicit act that authorizes the Custodian and Flink to manage the drone’s OI lifecycle autonomously going forward (v1.10). Flink’s OI Automation job begins watching this drone’s telemetry. | PUT /drones/{serial}/oi-automation |
| 3. Mission published | FLYsafe.software publishes a mission with (serial, callsign, time window, waypoints). The Custodian binds the mission and populates current_flight_state = PLANNED along with the mission window and waypoint total. |
POST /drones/{serial}/mission |
| 4. Drone airborne, telemetry flowing | GCS Plugin (or Cloud Broker) posts telemetry to RTTP, which the Custodian consumes to update position, flight_mode, armed in tenant.drones. It also flips current_flight_state = ACTIVE on first armed-airborne detection. The flight-monitoring job advances current_waypoint_index and current_progress_pct when the aircraft enters the waypoint_buffer_m radius (default 50 m) around the next expected waypoint — sequential matching, so a missed waypoint holds the index. |
POST /drones/{serial}/telemetryreads GET /drones/{serial}/waypoint-buffer |
| 5. OI regenerated | Flink’s OI Automation job predicts the trajectory and reads the drone’s FV and CV buffers, then computes three geometries: the trajectory expanded by FV buffers becomes the Flight Volume; expanded further by CV buffers becomes the Contingency Volume; the two are then combined into a single Submitted Volume polygon. All three are written to flights_binding (v1.13). Only the Submitted Volume is transmitted to FLYrtm DSS. On acceptance, current_oi_id / current_oi_state are updated (ACCEPTED then ACTIVE). Regeneration is triggered whenever the predicted trajectory would breach either volume. |
Internal Flink job reads GET /drones/{serial}/oi-defaultsreads GET /drones/{serial}/volumeswrites flights_bindingupdates current_oi block |
| 6. Operator or agent reviews | RTM Map renders the current OI geometry and reads the current-flight / current-oi summary blocks for the mission strip. Operator can adjust defaults mid-flight. (v1.13) RTM Map does not surface a manual “regenerate OI” action — regeneration is a backend-only process driven by Flink; MCP agents/ops tooling can still request a forced regeneration. OCC AI monitors OI conformance via the watch streams. | GET /drones/{serial}/oi/currentGET /drones/{serial}/current-oiGET /drones/{serial}/current-flightPOST /drones/{serial}/oi/regenerate |
| 7. Mission completes | Custodian flips current_flight_state = COMPLETED and current_oi_state = COMPLETED. Mission binding cleared; both blocks retained until the next mission binds. OI history retained for post-flight review and compliance evidence. |
DELETE /drones/{serial}/mission · GET /drones/{serial}/oi/history |
Flight_Live is the surveillance microservice that observes live_ads, live_community, and live_vision streams. In v1.6 it consults each drone’s ownership_flag_enabled (read from the Custodian API) to tag incoming records as own-aircraft or external.
v1.4 extracted “ghost” records at ingest. v1.5+ tags records instead and lets them flow through; each downstream consumer (Map, DAA, Blender, Analytics) applies its own filtering policy.
“Don’t throw away good data... let it flow through and filter it out at the very last minute before you when you don’t need it. Later on we’ll turn on the fusing and we will do fusing at multiple different locations. But if we don’t even have the data, we can’t fuse it.”
— Lindsay Mohr, Jun 15 Daily Technical Review
“We want to be able to go later on after the event, go and compare what was the MAVLink location versus what was the ADSB location and do lookups and telemetry. So why would we filter that information out, throw it away, not send it through?”
— Lindsay Mohr, same review
GET /drones/{serial}/ownership) to maintain an in-memory ownership index per tenant. The index is rebuilt on startup from the API; live changes flow in via either polling or the optional WebSocket watch endpoint. No MQTT subscription is required.
Per-stream tagging algorithms and the full Flight_Live spec live in the 859ze-4217 wiki subpage. The reframed v1.6 contract is: Flight_Live emits a unified surveillance_traffic stream where every record carries ownership_flagged: bool and owner_serial if matched. Downstream consumers decide what to do with the flag.
| Lifecycle Event | What Happens |
|---|---|
| Drone added in DLB | Next Import sync inserts the drone into tenant.drones with default RTM config. Identity created in Keycloak (<tenant>-<serial>). Published to AADMS. |
| Operator installs plugin | Mission Planner Plugin (or Chrome GCS Plugin) loaded, authenticates against the tenant’s Keycloak, ready to forward telemetry for any drone in scope. |
| Custodian boots | Server-side Custodian instance loads; calls GET /drones/{serial}/config for its drone; begins accepting telemetry posts from the plugin. |
| Telemetry frame arrives | Plugin POSTs to /drones/{serial}/telemetry. Custodian normalizes, writes live-state columns to tenant.drones, appends to InfluxDB. last_telemetry_at updated. |
| Pilot grants/toggles OI Automation authority | Frontend calls PUT /drones/{serial}/oi-automation. Row updated. This is the pilot-authority act (v1.10) — once granted, the Custodian owns current_flight_state/current_oi_state transitions autonomously until revoked. Custodian and Flink job pick up the change on their next poll. |
| Source rate degrades | Rate drops below the expected envelope or p95 latency exceeds threshold. Custodian sets data_source_status = DEGRADED. Map shows warning halo. OCC AI is notified via the data-source/watch stream. |
| Plugin disconnects | No telemetry for N × the expected interval. Custodian sets data_source_status = STALE then DISCONNECTED once an explicit disconnect is observed, and adapter_connected = false. Frontend and OCC see the state change. |
| Drone armed & airborne | Custodian detects armed + non-zero groundspeed from telemetry, flips current_flight_state = ACTIVE. Waypoint index advances as position matches successive waypoints within waypoint_buffer_m. |
| OI accepted by DSS | Flink OI Automation submits OI to FLYrtm DSS; on acceptance, Custodian writes current_oi_id / current_oi_state = ACCEPTED then ACTIVE. |
| Mission completes | Custodian flips current_flight_state = COMPLETED and current_oi_state = COMPLETED. Mission binding cleared via DELETE /drones/{serial}/mission. Both blocks retained until the next mission binds. OI history retained. |
| Drone retired | POST /drones/{serial}/retire. Row marked retired; live state cleared; Keycloak user disabled. |
Tracked across the wiki subpages under TECH-002/FSL RTTP.
/drones/{serial}/telemetry? (Maykon + Tim)app_user to per-identity DB roles for human-driven writes; trace each updated_at back to its JWT.| Question | Status | Resolution |
|---|---|---|
Current-flight state machine authority. Who owns transitions on current_flight_state? |
RESOLVED | Pilot-granted authority model (v1.10). The pilot grants OI Automation authority to the Custodian by flipping the per-drone toggle on the RTM Map Inventory page. Once granted, the Custodian owns state transitions autonomously (armed+airborne → ACTIVE, mission complete → COMPLETED, etc.). Pilot can revoke by flipping the toggle off. See callout in §5A. |
| Waypoint matching heuristic. How close to a waypoint is “reached”? | RESOLVED | Per-drone waypoint_buffer_m, default 50 m (v1.10). Sequential matching — a missed waypoint holds current_waypoint_index. Buffer settable per-drone on the RTM Map Inventory page. Respected by the Flink flight-monitoring jobs. |
| WebSocket watch endpoints vs polling. Ship WS endpoints, or rely on polling for the first cut? | RESOLVED | Shipped as optional companions to polling: /config/watch, /state/watch, /current-flight/watch, /current-oi/watch, /data-source/watch. Polling remains a valid fallback for every one of them. |
drones.drones. New columns ec_adsb_hex, adsb_out_device, adsb_hex_mode, surveillance_mode, daa_enabled, broadcast_binding, networkid_streaming_enabled, and read-only observed state adsb_observed_broadcasting / adsb_observed_last_icao_hex / adsb_observed_last_seen_by_cifib_at — closing the gap flagged in FSL_Database_Structure.html §4 between this schema and the RTM Map mockup's Conspicuity Configuration panel. Confirmed in the same review: the mockup's ec_callsign and ec_tc_reg are not new fields — they are local aliases for dlb_drones.inventory_number and dlb_drones.identification_number, already exposed as Callsign/TC_Registration in v_dtwin. No schema change needed for those two.drones.daa_rules (§4). One row per DAA rule per drone — backs the mockup's daa_rules[] array. Not part of v_dtwin (one-to-many); consumers query it directly.drones.oi_geometry removed from the design. It was specified in v1.11–v1.12 but never built. FV/CV/Submitted Volume storage for current and upcoming flights instead lives in the existing flights_binding table. Every “persisted in drones.oi_geometry” reference in §4, §5A, §7, and §📐 now reads flights_binding. This doc does not restate flights_binding's own schema.POST /drones/{serial}/oi/regenerate is no longer documented with Frontend as a caller (MCP/ops tooling only), and no new columns were added to support a manual-regenerate UI action. The current_mission_*/current_oi_* columns (v1.9) remain as they were — read/API/MCP surfaces, not new UI-driven state.Resolved against the RTM Map mockup during the developer gap-review (see FSL_Database_Structure.html §4) — the mockup's data model wins on both points below; the schema and API docs are updated to match it.
fv_lateral_buffer_m/fv_vertical_buffer_m and cv_lateral_buffer_m/cv_vertical_buffer_m are replaced by single fields fv_buffer_m and cv_buffer_m. Vertical separation from other traffic is no longer FV/CV's job — it is handled entirely by the NMAC/SWC minima below. drones.oi_geometry's flight_volume/contingency_volume polygons are still PolygonZ (they carry the trajectory's real altitude at each vertex), but that altitude is no longer buffer-expanded.nmac_distance_m and swc_distance_m scalars are replaced by nmac_h_crewed_m, nmac_v_crewed_m, nmac_h_uncrewed_m, nmac_v_uncrewed_m, and the same four for SWC — matching what the RTM Map mockup already implements. eval_distance_m is unchanged (still a single scalar).drones.drones schema (§4), the v_dtwin view, the dTwin JSON example (§3), the /drones/{serial}/volumes and /drones/{serial}/separation-defaults API contracts (§5A/I), the MCP tool table, and the Polygon Generation API requirements (§📐). The deployed rtm-tenant-schema.sql still needs a migration to catch up — tracked in FSL_Database_Structure.html.fv_*_buffer_m / cv_*_buffer_m) and the OI section ambiguously described “two nested prisms both submitted to DSS.” New drones.oi_geometry table (§4) persists all three per OI generation: the Flight Volume polygon, the Contingency Volume polygon, and the Submitted Volume — the single polygon that is the combination of FV and CV and is the one actually transmitted to FLYrtm DSS. All three are stored and exposed via the Custodian API, not derived on read.GET /drones/{serial}/oi/current/flight-volume and GET /drones/{serial}/oi/current/contingency-volume serve the two component geometries as standalone GeoJSON polygons. GET /drones/{serial}/oi/current is now explicitly documented as returning the Submitted Volume. current_oi.geometry_ref added to the dTwin payload linking to all three.drones.oi_geometry, with only the Submitted Volume transmitted to DSS — replacing the earlier “both prisms are submitted” description.PUT /drones/{serial}/oi-automation, FLYplan step 2, and added a callout in §5A.waypoint_buffer_m on drones.drones, default 50 m. When the aircraft enters this radius around the next expected waypoint, it is considered to have reached it. Sequential matching — a missed waypoint holds the index. New APIs GET/PUT /drones/{serial}/waypoint-buffer with server-side clamp 5–500 m.current_flight block in the dTwin payload backed by seven new columns on drones.drones (current_mission_id, current_flight_state, window start/end, waypoint index/total, progress pct). Populated by FLYsafe.software at mission publish; progress updated by the Custodian from telemetry.current_oi block backed by seven new columns (current_oi_id, current_oi_state, current_oi_source, window start/end, last/next regeneration timestamps). Written by the Flink OI Automation job on every generation/regeneration; the Custodian mirrors it into the dTwin.GET /drones/{serial}/current-flight and GET /drones/{serial}/current-oi return just the respective summary blocks. Companion WebSocket endpoints /current-flight/watch and /current-oi/watch stream state transitions.fv_lateral_buffer_m, fv_vertical_buffer_m, cv_lateral_buffer_m, cv_vertical_buffer_m — supersede the v1.6–v1.7 flat flight_volume_m / contingency_vol_m fields so FV and CV can be sized independently in each dimension. New GET/PUT /drones/{serial}/volumes API with a clamp enforcing CV ≥ FV. OI Automation (FLYplan step 5) uses these buffers to compute the Flight and Contingency Volume geometries (see v1.11 for the storage/API model for those geometries).default-pilot (name + FLYsafe.software GUID, pre-fills mission creation) and separation-defaults (NMAC / SWC / EVAL distances, metres, with nmac < swc < eval enforced server-side).data_source_mechanism, data_source_rate_hz, data_source_latency_ms_p50/p95, and a categorical data_source_status (HEALTHY / DEGRADED / STALE / DISCONNECTED) with defined transition triggers and consumer behaviour for each.drone.get_default_pilot, drone.set_default_pilot, drone.get_separation_defaults, drone.set_separation_defaults, drone.get_data_source, drone.get_data_source_history, drone.get_volumes, drone.set_volumes, drone.get_current_flight, drone.get_current_oi, drone.get_waypoint_buffer, drone.set_waypoint_buffer.tenant.drones Postgres. APIs are the contract for everything else (frontend reads/writes, Custodian writes from telemetry, MCP agent tools).tenant.drones and managed through the Custodian APIs. The Custodian itself fetches its own configuration at boot via these same APIs.<strong>, <em>, <code>) inside SVG <text> elements with valid <tspan> equivalents — restoring rendering of the Solution Concept and System Architecture diagrams.§4 established that three geometries — Flight Volume, Contingency Volume, and the Submitted Volume — are stored in flights_binding (v1.13) and exposed via the §5A endpoints. This section specifies the API that actually computes those three polygons from a predicted trajectory and the drone’s FV/CV buffer defaults. It does not yet exist; JC is building it. Everything below is the requirements contract for that work — not a description of a shipped API.
Flink’s OI Automation job (FLYplan step 5, §7) predicts the trajectory and calls this API to get the three polygons back. Flink then submits the Submitted Volume to FLYrtm DSS and, on acceptance, is responsible for persisting all three geometries to flights_binding. This API’s job is purely geometric — trajectory + buffers in, three polygons out. It does not talk to DSS and does not write to Postgres itself.
| Endpoint | Purpose | Caller |
|---|---|---|
| POST/drones/{serial}/oi-geometry/compute | Compute the Flight Volume, Contingency Volume, and Submitted Volume polygons for one predicted trajectory. Stateless — does not write to flights_binding; the caller (Flink) persists the result there after DSS submission. | Flink (OI Automation job) |
{
"tenant_id": "airmarket",
"serial_number": "1581F5BBB1F2A",
"trajectory": [ // predicted path, ordered, ≥ 2 points
{ "lat": 53.5461, "lon": -113.4938, "alt_msl_m": 720.4, "t": "2026-06-16T18:42:00Z" },
{ "lat": 53.5480, "lon": -113.4901, "alt_msl_m": 725.1, "t": "2026-06-16T18:43:30Z" }
// ...
],
"fv_buffer_m": 56.0, // horizontal only (v1.12)
"cv_buffer_m": 150.0
}
{
"flight_volume": { "type": "Polygon", "coordinates": [ /* GeoJSON PolygonZ */ ] },
"contingency_volume": { "type": "Polygon", "coordinates": [ /* GeoJSON PolygonZ */ ] },
"submitted_volume": { "type": "Polygon", "coordinates": [ /* GeoJSON PolygonZ — see open question below */ ] },
"computed_at": "2026-06-16T18:42:18.600Z"
}
| Requirement | Detail |
|---|---|
| FV polygon | Horizontal only (v1.12): the trajectory linestring expanded outward by fv_buffer_m. The polygon keeps the trajectory’s own altitude at each vertex (PolygonZ) — there is no vertical buffer expansion. Vertical separation from other traffic is handled by the NMAC/SWC crewed/uncrewed minima (§5I), not by FV/CV. |
| CV polygon | Same construction as FV, using cv_buffer_m instead. Must fully contain the FV polygon horizontally — the API should validate this and reject (422) any input where cv_buffer_m < fv_buffer_m. |
| Submitted Volume | The single polygon that is the combination of FV and CV, structured however FLYrtm DSS’s OI submission format requires it (see open question below). This is the only one of the three actually transmitted to DSS. |
| Determinism | Same trajectory + same buffers must always produce the same three polygons, byte-for-byte. Regeneration diffing (§7 step 5) depends on this. |
| Latency | Called on every OI regeneration tick (§7 step 5), so this must return well within Flink’s regeneration cadence. Target: p95 < 200 ms per call. |
| No side effects | Purely a compute step — must not write to flights_binding, must not call DSS, must not read/write any drone state. Keeps the geometry math independently testable. |
submitted_volume actually contains and must be confirmed against the DSS integration spec before this is built.flights_binding's geometry columns) and how many decimal places of lat/lon precision are required for DSS acceptance.PolygonZ (v1.12: FV/CV no longer buffer vertically, but the polygon still carries the trajectory’s real altitude at each vertex) — current_flight telemetry elsewhere in this doc uses MSL; confirm this API should too.