Annex A — Mappings to ONVIF, MAVLink, MISB ST 0601 and the DJI Cloud APILink to the section Annex A — Mappings to ONVIF, MAVLink, MISB ST 0601 and the DJI Cloud API

On this page

Maturity: Draft · Informative (normative for gateway authors who claim a mapping). Items marked ⚠ require verification against the primary specification.

A.1 ONVIF ↔ UNIVIPLink to the section A.1 ONVIF ↔ UNIVIP

A.1.1 Services and resourcesLink to the section A.1.1 Services and resources

ONVIF UNIVIP
Device GetDeviceInformation GET /devices/{id} → manufacturer, model, firmware_version, serial_number
Device GetCapabilities, GetServices GET /devices/{id}/capabilities
Device GetSystemDateAndTime telemetry.time + RFC 3339 timestamps
Media2 GetProfiles, GetStreamUri capabilities.media.streams[]; rtsp.url when RTSP offered
WebRTC (Profile V) signal envelopes / POST .../webrtc
Uplink Device-initiated session channel
Events CreatePullPointSubscription / MQTT binding Session channel event envelopes / MQTT alternative
PTZ GetConfigurations, GetNodes capabilities.payloads[].gimbal, .zoom
PTZ AbsoluteMove camera.gimbal.set_orientation (+ camera.zoom.set)
PTZ RelativeMove camera.gimbal.move_relative
PTZ ContinuousMove with Timeout camera.gimbal.move_continuous with expires_at
PTZ Stop camera.gimbal.stop, camera.zoom.stop
PTZ GotoHomePosition camera.gimbal.recenter
PTZ GeoMove ⚠ camera.gimbal.point_at
Imaging SetImagingSettings camera.imaging.set
Imaging Move (focus) camera.focus.set, camera.focus.trigger
Recording control (Profile G) camera.recording.start/stop for on-board recording; client recording is the VMS's own

A.1.2 PTZ space conversionLink to the section A.1.2 PTZ space conversion

ONVIF generic pan/tilt space is normalized [-1, 1]. A gateway exposing a UNIVIP gimbal as ONVIF PTZ maps yaw_deg ∈ [yaw.min, yaw.max] linearly onto pan ∈ [-1, 1] and pitch_deg onto tilt likewise, and zoom_ratio ∈ [min_ratio, max_ratio] onto zoom ∈ [0, 1]. The gateway MUST publish an ONVIF SphericalDegrees space when it exposes EARTH frame angles. Continuous velocities map rate_degps / max_rate_degps.

A.1.3 Event topicsLink to the section A.1.3 Event topics

UNIVIP topic ONVIF topic (suggested)
vehicle.health.fault tns1:Device/HardwareFailure/* by component, else tns1:Device/Trigger/DigitalInput ⚠
vehicle.camera.recording.started/stopped tns1:RecordingHistory/Recording/State ⚠
vehicle.analytics.object.detected tns1:RuleEngine/ObjectDetector/Object with Profile M object metadata
vehicle.link.lost, vehicle.battery.*, vehicle.operation.*, vehicle.navigation.*, vehicle.control.* No ONVIF equivalent → vendor namespace tns1:Device/Trigger/univip/<topic> or UNIVIP namespace univip:

A.1.4 Metadata streamLink to the section A.1.4 Metadata stream

UNIVIP telemetry MAY be carried in the ONVIF metadata stream as tt:MetadataStream/tt:Extension/univip:Telemetry containing the JSON telemetry sample (as a CDATA string) so a Profile M/T client records it. The ONVIF tt:GeoLocation element SHOULD be populated from position (lat, lon, altitude_msl_m as elevation) and tt:GeoOrientation from motion (roll, pitch, yaw) ⚠ element names.

A.2.1 TelemetryLink to the section A.2.1 Telemetry

MAVLink UNIVIP Conversion
GLOBAL_POSITION_INT.lat/lon position.latitude_deg/longitude_deg ÷ 1e7
GLOBAL_POSITION_INT.alt position.altitude_msl_m mm → m (MSL)
GLOBAL_POSITION_INT.relative_alt position.altitude_rel_m mm → m
GLOBAL_POSITION_INT.alt + (GPS_RAW_INT.alt_ellipsoid − GPS_RAW_INT.alt) position.altitude_ellipsoid_m mm → m: the fused MSL altitude plus the receiver's geoid separation. alt_ellipsoid alone is the raw receiver height and disagrees with the fused MSL altitude.
DISTANCE_SENSOR (down) / RANGEFINDER position.altitude_agl_m (agl_source: RANGEFINDER) cm → m / m
GLOBAL_POSITION_INT.vx/vy/vz motion.velocity_north/east/down_mps cm/s → m/s
GLOBAL_POSITION_INT.hdg motion.heading_deg cdeg → deg; 65535 = unknown → omit
ATTITUDE.roll/pitch/yaw motion.roll_deg/pitch_deg/yaw_deg rad → deg; yaw wrapped to [0,360)
ATTITUDE.rollspeed/... motion.*_rate_degps rad/s → deg/s
BATTERY_STATUS.battery_remaining power.battery_pct %
BATTERY_STATUS.voltages[] sum / SYS_STATUS.voltage_battery power.voltage_v mV → V
BATTERY_STATUS.current_battery power.current_a cA → A
BATTERY_STATUS.time_remaining power.remaining_endurance_s s
RADIO_STATUS.rssi, remrssi link.rssi_dbm (vendor-scaled) / signal_quality_pct vendor; declare
HEARTBEAT.system_status, base_mode, custom_mode, EXTENDED_SYS_STATE.landed_state vehicle.vehicle_state, armed, flight_mode see A.2.2
HOME_POSITION vehicle.home 1e7 / mm
GPS_RAW_INT.fix_type health.gnss.fix 0,1→NONE; 2→2D; 3→3D; 4→DGPS; 5→RTK_FLOAT; 6→RTK_FIXED
GPS_RAW_INT.satellites_visible, eph health.gnss.satellites, hdop eph/100 ⚠
SYS_STATUS.onboard_control_sensors_health health.sensors, health.faults[] bitmask → named entries
STATUSTEXT severity ≥ WARNING vehicle.health.fault / vehicle.operation.emergency map MAV_SEVERITY
STATUSTEXT text at the start of RTL vehicle.navigation.returning_home.reason ⚠ MAVLink has no failsafe-reason message; the reason is inferred from autopilot-specific wording (battery, link/RC/GCS loss, geofence), and is CLIENT when the gateway commanded the return, OTHER when nothing matches
GIMBAL_DEVICE_ATTITUDE_STATUS.q + flags payloads[].gimbal quaternion → Euler deg. With GIMBAL_DEVICE_FLAGS_YAW_IN_EARTH_FRAME (or yaw lock) the yaw is an EARTH azimuth; otherwise it is relative to the vehicle and is reported as BODY, or converted to EARTH by adding the vehicle heading. MAVLink's "vehicle frame" keeps pitch and roll level, which is exactly UNIVIP BODY (Part 10 §10.3).
CAMERA_SETTINGS.zoomLevel payloads[].zoom_ratio ⚠ percent of the zoom range; ratio = 1 + (max − 1) × percent / 100, where the maximum ratio is not in CAMERA_INFORMATION and must come from configuration or the camera definition file. Without it, zoom is not advertised.
CAMERA_INFORMATION.focal_length, sensor_size_h/v payloads[].hfov_deg/vfov_deg 2·atan(sensor/2f)
CAMERA_CAPTURE_STATUS.video_status payloads[].recording.active
VIDEO_STREAM_INFORMATION media.streams[] uri → rtsp.url

A.2.2 Vehicle state derivation (AIR domain; ArduSub uses the same messages, read DEPLOYING/OPERATING/HOLDING as dive/transit/depth-hold)Link to the section A.2.2 Vehicle state derivation (AIR domain; ArduSub uses the same messages, read DEPLOYING/OPERATING/HOLDING as dive/transit/depth-hold)

Condition vehicle_state
Not armed, landed_state = ON_GROUND or UNDEFINED, no flight since arming IDLE
Armed, ON_GROUND, no flight since arming PREDEPLOY
ON_GROUND after a flight since the last arming RECOVERED
landed_state = TAKEOFF DEPLOYING
IN_AIR, mode RTL/Return (or Smart RTL), within 5 m of home and at rest for 10 s (a return with land_on_arrival: false has arrived; both autopilots stay in RTL) HOLDING
IN_AIR, mode RTL/Return (or Smart RTL) otherwise RETURNING
IN_AIR, ground speed < 0.5 m/s and vertical speed < 0.5 m/s for > 2 s, in a hold mode HOLDING
IN_AIR otherwise OPERATING
landed_state = LANDING RECOVERING
system_status = EMERGENCY or FLIGHT_TERMINATION EMERGENCY

MAV_STATE_CRITICAL means a failsafe is active while the vehicle can still navigate; PX4 and ArduPilot report it for every active failsafe, including a low-battery return. It is not EMERGENCY: the state follows the mode (typically RETURNING).

Hold modes are autopilot-specific: PX4 AUTO_LOITER (Hold, in which PX4 also flies a reposition) and POSCTL; ArduPilot copter LOITER, BRAKE, POSHOLD and GUIDED (a vehicle in GUIDED that has reached its target hovers), rover HOLD. A pilot override is a switch into a manual mode the gateway did not command (PX4 MANUAL, ALTCTL, POSCTL, STABILIZED, ACRO; ArduPilot copter STABILIZE, ALT_HOLD, LOITER, POSHOLD, ACRO, SPORT, DRIFT), or lift-off in one, judged on HEARTBEAT alone (a landed-state report can be newer than the last heartbeat), or RC input reported by the autopilot.

STATUSTEXT at WARNING or worse becomes vehicle.health.fault events, not the critical faults of Part 11 §11.2: autopilots send pre-arm and arming refusals at CRITICAL, which would otherwise block client commands. The failsafe reason for vehicle.navigation.returning_home comes from text where the autopilot gives it: ArduPilot says "Battery Failsafe" and "GCS Failsafe"; PX4 1.17 names the cause only through its events protocol (which needs PX4's event metadata), so a PX4 link-loss return reads OTHER from text alone.

A.2.3 CommandsLink to the section A.2.3 Commands

UNIVIP MAVLink
vehicle.hold PX4: MAV_CMD_DO_REPOSITION with latitude, longitude and altitude all NaN (pause in place; selecting Hold mode again does not stop a reposition already flying in Hold). ArduPilot copter: BRAKE (LOITER follows the throttle stick and descends with no RC input). Either way the vehicle MUST keep its altitude without pilot input.
vehicle.return_home MAV_CMD_NAV_RETURN_TO_LAUNCH; RTH altitude via param RTL_RETURN_ALT (PX4, m) / RTL_ALT (ArduCopter before 4.7, cm) or RTL_ALT_M (ArduCopter 4.7, m); land_on_arrival: false via RTL_LAND_DELAY (PX4, −1 = hover) / RTL_ALT_FINAL (cm) or RTL_ALT_FINAL_M (ArduCopter 4.7, m; > 0 = hover at that height). Parameter names and units change between firmware versions; choose them by the reported version, with a fallback. The command completes on landing, or, without landing, when the vehicle holds over home (A.2.2). The gateway MUST reject altitude_rel_m or land_on_arrival if it cannot set them, MUST read the original values first, and MUST restore them when the commanded return ends or is rejected, so that later failsafe returns behave as configured
vehicle.goto PX4: MAV_CMD_DO_REPOSITION, whose altitude PX4 1.17 always reads as AMSL whatever the frame: convert a REL target with the home altitude, and reject AGL. ArduPilot: SET_POSITION_TARGET_GLOBAL_INT in Guided with frame MAV_FRAME_GLOBAL_RELATIVE_ALT_INT (REL), MAV_FRAME_GLOBAL_INT (MSL) or MAV_FRAME_GLOBAL_TERRAIN_ALT_INT (AGL).
vehicle.move_relative SET_POSITION_TARGET_LOCAL_NED with MAV_FRAME_BODY_OFFSET_NED (BODY) or MAV_FRAME_LOCAL_OFFSET_NED (NED)
vehicle.set_heading MAV_CMD_CONDITION_YAW; complete when the heading is reached and the yaw rate has settled (≤ 5°/s)
vehicle.land MAV_CMD_NAV_LAND
vehicle.takeoff MAV_CMD_NAV_TAKEOFF (after arm)
vehicle.mission.upload/start/pause/resume/abort Mission protocol upload; MAV_CMD_MISSION_START; MAV_CMD_DO_PAUSE_CONTINUE; abort → vehicle.hold semantics
camera.gimbal.set_orientation MAV_CMD_DO_GIMBAL_MANAGER_PITCHYAW (EARTH ↔ GIMBAL_MANAGER_FLAGS_YAW_LOCK), after taking primary control with MAV_CMD_DO_GIMBAL_MANAGER_CONFIGURE (PX4 denies commands from any other sender; BUSY when another system holds control). An omitted axis is sent at its current angle: ArduPilot ignores a pitch/yaw pair with one NaN member.
camera.gimbal.move_continuous MAV_CMD_DO_GIMBAL_MANAGER_PITCHYAW with rates and NaN angles (PX4 1.17 has no GIMBAL_MANAGER_SET_PITCHYAW handler)
camera.zoom.set / move_continuous MAV_CMD_SET_CAMERA_ZOOM (ZOOM_TYPE_RANGE / ZOOM_TYPE_CONTINUOUS)
camera.focus.* MAV_CMD_SET_CAMERA_FOCUS
camera.recording.start/stop MAV_CMD_VIDEO_START_CAPTURE / STOP_CAPTURE
camera.snapshot MAV_CMD_IMAGE_START_CAPTURE count=1
camera.mode.set MAV_CMD_SET_CAMERA_MODE

COMMAND_ACK.result maps: ACCEPTED→ACCEPTED; TEMPORARILY_REJECTED→REJECTED/BUSY; DENIED→REJECTED/SAFETY; UNSUPPORTED→REJECTED/UNSUPPORTED_COMMAND; FAILED→ACCEPTED then FAILED/INTERNAL; IN_PROGRESS→ACCEPTED then EXECUTING with progress where the autopilot reports it; CANCELLED→CANCELLED; COMMAND_LONG_ONLY/COMMAND_INT_ONLY → retry once in the other form, then REJECTED/UNSUPPORTED_COMMAND. The synchronous answer waits for the acknowledgment (Part 10 §10.1). A gateway waits at least 3 s for an acknowledgment and retransmits once (MAVLink command protocol); no acknowledgment after that is REJECTED/LINK_LOST.

A gateway on the ground-station side of the radio link observes vehicle link loss and stale telemetry as the same thing; it raises vehicle.telemetry.stale and does not advertise vehicle.link.lost (Part 08 §8.3).

Control leases have no MAVLink equivalent; the gateway implements them, and holds gimbal control through the gimbal manager's primary controller (MAV_CMD_DO_GIMBAL_MANAGER_CONFIGURE, GIMBAL_MANAGER_STATUS.primary_control_sysid).

Deployment notes: PX4 AUX-output gimbals report GIMBAL_MANAGER_INFORMATION limits in degrees in fields defined in radians, as ±range while moving within ±range/2; a gateway should check limits for plausibility. PX4 answers only the first peer of a MAVLink UDP link for the link's lifetime, so a gateway should listen on a fixed port (udpin) rather than an ephemeral udpout port that changes on restart. Validated against PX4 1.17.0 and ArduCopter 4.7.2 SITL (docs/sitl-validation.md).

A.3 MISB ST 0601 ↔ UNIVIPLink to the section A.3 MISB ST 0601 ↔ UNIVIP

One UAS Datalink Local Set per video frame, Precision Time Stamp first and checksum (tag 1) last. Tag numbers below were checked against an independent open-source decoder and ST 0902 worked examples; the standard itself could not be consulted for 0.2, and the points marked ⚠ remain to be confirmed in the formal review (docs/prior-art.md §5).

ST 0601 tag Name UNIVIP
2 Precision Time Stamp the frame's device time (Part 09 §9.5), µs since the epoch ⚠ POSIX time vs MISP Time (ST 0603)
3 Mission ID session_id
4 Platform Tail Number device.serial_number
5 Platform Heading Angle motion.heading_deg
6, 7 Platform Pitch / Roll Angle motion.pitch_deg, motion.roll_deg (±20°, ±50°); beyond that range the out-of-range indicator, with the value in 90, 91
10 Platform Designation device.model
11 Image Source Sensor payload_id
13, 14 Sensor Latitude / Longitude position.latitude_deg/longitude_deg
15 Sensor True Altitude position.altitude_msl_m
16, 17 Sensor Horizontal / Vertical FOV payloads[].hfov_deg/vfov_deg
18, 19, 20 Sensor Relative Azimuth / Elevation / Roll the gimbal orientation relative to the platform body (full attitude): convert the UNIVIP gimbal to EARTH (for BODY, add heading_deg to the yaw, Part 10 §10.3), then remove the platform attitude, with both rotations applied in Z-Y-X order (heading, pitch, roll). Subtracting the heading from the yaw is correct only for a level platform.
21 Slant Range geospatial.slant_range_m
23, 24, 25 Frame Center Lat / Lon / Elevation geospatial.frame_center
26–33 Offset Corner Lat / Lon 1–4 geospatial.footprint, as offsets from the frame center when every offset is within ±0.075°; otherwise 82–89. Corner order: upper left, then clockwise ⚠
48 Security Local Set out of scope (client policy)
65 UAS Datalink LS Version Number the ST 0601 edition the encoder follows (the reference recorder writes 17) ⚠
67, 68 Alternate Platform Latitude / Longitude controller or dock position (not modeled)
72 Event Start Time – UTC start of the operation (vehicle.operation.started), when known
74 VMTI Local Set (ST 0903) vehicle.analytics.object.detected (target_id, bbox, geolocation, confidence)
75 Sensor Ellipsoid Height position.altitude_ellipsoid_m
82–89 Corner Lat / Lon Point 1–4 (Full) geospatial.footprint when the offsets do not fit
90, 91 Platform Pitch / Roll Angle (Full) motion.pitch_deg/roll_deg beyond the range of 6 and 7

There is no full-range heading tag (92 is the Angle of Attack). A footprint the client estimated itself MUST NOT be exported in tags 23–33 or 82–89 unless the export also marks it as estimated; the reference recorder estimates footprints only on request and records the fact in its manifest. Strings are encoded in UTF-8 ⚠ (earlier editions say ISO 646).

A.4 DJI Cloud API ↔ UNIVIPLink to the section A.4 DJI Cloud API ↔ UNIVIP

Status: Draft for merging into Annex A (spec/annex-a-mappings.md) for specification 0.2. Informative.

Source: DJI Cloud API documentation v1.11.3 (dji-sdk/Cloud-API-Doc, commit 4ec6b0c, 2024-11-07), dock-to-cloud MQTT thing model for DJI Dock (Matrice 30/30T) and DJI Dock 2 (Matrice 3D/3TD), cross-checked against the reference server dji-sdk/DJI-Cloud-API-Demo (commit bef525c). Field names, methods, units and enumerations below are DJI's; descriptions are paraphrased.

Implementation: the reference gateway's dji_cloud adapter (../gateway/univip_gateway/adapters/dji_cloud.py). Verified only against a fake dock built from the same documents (../gateway/univip_gateway/fakes/dji_dock.py); no real dock has been tested. ⚠ marks a mapping that is lossy, rests on an undocumented convention, or is ambiguous; J-numbers refer to the implementation findings for spec 0.2.

Model. In DJI terms the dock is a gateway device and the aircraft its sub-device; a UNIVIP device represents the aircraft together with its dock (Part 02, "Dock"). The UNIVIP gateway takes the role DJI calls the cloud server: it is an MQTT client of the broker the dock is bound to. DJI uses these topics ({sn} is the device's serial number, {gw} the dock's):

Topic Direction Use
thing/product/{sn}/osd dock → cloud Periodic properties (pushMode 0), documented at 0.5 Hz
thing/product/{sn}/state dock → cloud Properties pushed on change only (pushMode 1) ⚠ a late subscriber misses them (J14)
thing/product/{gw}/services, …/services_reply cloud → dock, reply Commands; reply data.result 0 = success
thing/product/{gw}/events, …/events_reply dock → cloud, reply Events; reply required when need_reply = 1
sys/product/{gw}/status, …/status_reply dock → cloud, reply update_topo: gateway and sub-device online, topology
thing/product/{gw}/drc/up, …/drc/down both Live flight controls (DRC): high-rate OSD, heartbeat, brake, sticks

Every message carries tid (transaction), bid (business flow), timestamp (milliseconds, send time) and, for services and events, method. The gateway matches a services_reply to its request by tid. MQTT 3.1.1 at QoS 1 is used for services and replies (DJI's overview cites MQTT 5.0 concepts but the reference server uses a 3.1.1 client).

A.4.1 TelemetryLink to the section A.4.1 Telemetry

DJI UNIVIP Conversion
aircraft latitude, longitude position.latitude_deg/longitude_deg degrees; omitted when 0/0 or when the fix is NONE
aircraft height position.altitude_ellipsoid_m m above the WGS84 ellipsoid (DJI: takeoff point's ellipsoid height + elevation)
aircraft elevation position.altitude_rel_m m above the takeoff point ⚠ UNIVIP says "home/launch"; DJI's home can differ from the takeoff point (return_specific_home, multi-dock) (J2)
height − N position.altitude_msl_m, geoid_model only with a configured geoid undulation N at the site ⚠ the thing model has no MSL height (J2)
— position.altitude_agl_m not available ⚠ no height above ground in the thing model
DRC osd_info_push.latitude/longitude (1–30 Hz) position.latitude_deg/longitude_deg while live flight controls are active
DRC osd_info_push.height change applied to the altitudes above ⚠ reference undocumented: English "Flight altitude" with unit "degree", Chinese 海拔 (above sea level). Only its change since the last thing-model sample is used (J13)
position_state.is_fixed, quality, gps_number, rtk_number health.gnss.fix, health.gnss.satellites, position.source quality 10 or is_fixed 2 → RTK_FIXED; is_fixed 1 with rtk_number > 0 → RTK_FLOAT ⚠; gps_number = rtk_number = 0 → NONE; otherwise 3D ⚠ (no 2D/3D distinction, no HDOP, no accuracy); satellites = gps_number; source RTK_GNSS for RTK fixes, else GNSS
attitude_head motion.heading_deg, motion.yaw_deg degrees from true north, east positive, (−180, 180] → [0, 360)
attitude_pitch, attitude_roll motion.pitch_deg, motion.roll_deg degrees ⚠ sign convention undocumented; taken as UNIVIP's (J13)
horizontal_speed motion.ground_speed_mps m/s
vertical_speed motion.velocity_down_mps m/s, negated ⚠ sign undocumented; taken as positive up (J13)
DRC speed_x, speed_y, speed_z motion.ground_speed_mps = √(x² + y²) ⚠ axis frame undocumented: no north/east/down components and no course_deg (J13)
battery.capacity_percent power.battery_pct %
battery.remain_flight_time power.remaining_endurance_s s
battery.batteries[] index, capacity_percent, voltage, temperature, loop_times power.batteries[] id, pct, voltage_v, temperature_c, cycles mV → V; 32767 means unavailable and is omitted
lowest battery.batteries[].voltage power.voltage_v ⚠ packs in parallel; no pack voltage or current is reported
battery.return_home_power, battery.landing_power; state low_battery_warning_threshold, serious_low_battery_warning_threshold — ⚠ no UNIVIP member; vehicle.battery.low/critical use the gateway's configured thresholds (J8)
dock drone_charge_state.capacity_percent, drone_battery_maintenance_info.batteries[] power.battery_pct, power.batteries[] while the aircraft is powered off in the dock
dock wireless_link sdr_link_state, 4g_link_state, sdr_quality, 4g_quality link.state, link.signal_quality_pct, link.transport best quality of the links up, 0–5 × 20 %; below 30 % → DEGRADED; SDR up → RADIO, 4G only → CELLULAR ⚠
DRC delay_info_push.sdr_cmd_delay link.latency_ms ms
aircraft mode_code, dock drone_in_dock vehicle.vehicle_state, domain_state, flight_mode A.4.2; flight_mode is the mode_code name in upper snake case (e.g. AUTO_RETURNING_TO_HOME)
state home_latitude, home_longitude vehicle.home degrees; altitude_msl_m only with N, taken from the takeoff point ⚠ GeoPoint has no altitude_ellipsoid_m (J2)
— vehicle.armed not reported
{type-subtype-gimbalindex}.gimbal_pitch/gimbal_roll/gimbal_yaw; DRC gimbal_pitch/roll/yaw payloads[].gimbal (frame: EARTH) degrees ⚠ the yaw reference is undocumented; taken as true north (DJI gimbal attitude convention) (J13)
cameras[].zoom_factor payloads[].zoom_ratio ⚠ documented range 2–200 (IR 2–20); the base field of view is undocumented, so no hfov_deg/vfov_deg (J7)
cameras[].recording_state, record_time payloads[].recording.active, started_at started_at = receipt time − record_time
cameras[].camera_mode — read before recording or a photo (A.4.3)
events hms health.faults[], vehicle.dock.fault A.4.4
aircraft control_source, payloads[].control_source (DJI Dock: dock drone_authority_info) vehicle.control.pilot_override A.4.5
aircraft rc_lost_action (DJI Dock/M30), else the value sent with takeoff_to_point vehicle.link.lost failsafe_action 0 → HOLD, 1 → LAND, 2 → RETURN_HOME
dock cover_state, drone_charge_state.state vehicle.dock.state_changed from/to 0 → CLOSED (CHARGING while charging), 1 → OPEN, 2 (half open) → OPENING or CLOSING by the previous state ⚠, 3 → FAULT (J10)
aircraft powered off in the dock position = dock latitude, longitude, height; link.state UNKNOWN ⚠ the dock's surveyed position stands in for the aircraft's
message timestamp — send time, not time of validity; observations are stamped on receipt ⚠
thing-model osd rate 0.5 Hz telemetry.rate_hz.max 0.5 ⚠ Part 07 §7.11 asks for ≥ 2 Hz deployed; only met with DRC osd_frequency (J1)

A.4.2 Vehicle state derivation (AIR)Link to the section A.4.2 Vehicle state derivation (AIR)

Aircraft mode_code vehicle_state domain_state
0 Standby, 13 Upgrading IDLE DOCKED when dock drone_in_dock = 1, else LANDED ⚠ DOCKED is not used as vehicle_state (J4)
1 Takeoff preparation, 2 Takeoff preparation completed PREDEPLOY LANDED
4 Automatic takeoff DEPLOYING TAKING_OFF
9 Auto returning to home RETURNING FLYING
10 Automatic landing RECOVERING LANDING
11 Forced landing EMERGENCY (kind: FORCED_LANDING) ⚠ FLYING
12 Three-blade landing EMERGENCY (kind: MOTOR_FAILURE) FLYING
14 Not connected, or no aircraft osd within the stale period while the dock is heard UNKNOWN, link.state: LOST, no position —
Any other airborne mode (3, 5–8, 15–20) HOLDING when horizontal speed < 0.5 m/s and vertical speed < 0.3 m/s for ≥ 1.5 s and no flyto is in progress, else OPERATING HOVERING / FLYING
Aircraft absent from update_topo (powered off) and dock drone_in_dock = 1 IDLE DOCKED

RECOVERED is never produced: DJI reports no arming state. A silent dock is a stale device (vehicle.telemetry.stale, vehicle.device.offline); a silent aircraft under a dock that is still heard is a lost vehicle link (vehicle.link.lost), which settles finding F9 for dock gateways.

A.4.3 CommandsLink to the section A.4.3 Commands

UNIVIP DJI Notes
vehicle.takeoff takeoff_to_point Target = the dock's position; target_height = takeoff ellipsoid height + altitude_rel_m (documented range 2–1500 m). security_takeoff_height (≥ 20 m), rth_altitude, rc_lost_action, commander_*, max_speed come from configuration ⚠ the aircraft first climbs to max(security_takeoff_height, commander_flight_height), so a target below 20 m is reached from above; dock preparation (cover, power-on) shows as PREDEPLOY (J15)
vehicle.goto fly_to_point (fly_to_id, max_speed, one point with ellipsoid height); fly_to_point_update while one is in progress altitude_rel_m + takeoff ellipsoid height; altitude_msl_m + N (rejected INVALID_PARAMETERS without N); altitude_agl_m rejected (no terrain model) (J2); heading_deg rejected (no heading); max_speed integer 0–15 m/s, rounded ⚠; below 20 m the aircraft first climbs to 20 m ⚠; transit at commander_flight_height ⚠ so the path is not the straight segment the geofence path check assumes
vehicle.move_relative through vehicle.goto yaw_deg rejected
vehicle.hold depends on what moves the aircraft: returning → return_home_cancel; wayline → flighttask_pause; flyto or takeoff → fly_to_point_stop; otherwise, with DRC active, drone_emergency_stop on drc/down; already hovering → nothing to send ⚠ there is no single documented "hover now" service. drone_emergency_stop is a brake, not a motor stop: it is documented as cancelling a forced landing (J3)
vehicle.cancel as vehicle.hold ⚠ during the dock's takeoff preparation (PREDEPLOY) no documented service aborts the launch (J15)
vehicle.return_home return_home altitude_rel_m rejected: the return altitude is fixed at takeoff (rth_altitude) ⚠; land_on_arrival: false rejected: a dock return always lands in the dock
vehicle.set_heading — Not offered: only stick-level drone_control over DRC could yaw the aircraft
vehicle.land — Not offered: drc_force_landing and drc_emergency_landing ignore obstacles and need manual recovery; a dock aircraft lands through return_home
vehicle.mission.* flighttask_prepare / flighttask_execute / flighttask_pause / flighttask_recovery / flighttask_undo with a WPML KMZ fetched from a URL Not implemented ⚠ needs mission → WPML conversion and an object store
camera.gimbal.set_orientation (EARTH) camera_look_at with locked: false, aimed at a point 200 m along the requested line of sight ⚠ indirect (accuracy follows the position estimate); whether look-at keeps tracking is undocumented; gimbal limits are not reported. locked: true is never sent because it turns the aircraft (Part 10 §10.2) (J6)
camera.gimbal.move_continuous / camera.gimbal.stop camera_screen_drag (pitch_speed, yaw_speed in rad/s, locked: false), repeated every 0.5 s; stop = both speeds 0 ⚠ how long one drag lasts and the sign of each speed are undocumented (J13)
camera.gimbal.recenter gimbal_reset reset_mode 0
camera.gimbal.point_at (Part 12) camera_look_at Not implemented (the reference gateway has no Part 12)
— camera_aim (normalized image x, y), camera_frame_zoom (image region) No UNIVIP command (J6)
camera.zoom.set camera_focal_length_set (camera_type: zoom, zoom_factor) Documented range 2–200 (IR 2–20) ⚠ the camera's real range is not reported (J7)
camera.zoom.move_continuous / camera.zoom.stop — Not offered
camera.recording.start / stop camera_mode_switch to 1 when needed, camera_recording_start / camera_recording_stop No media id is returned ⚠
camera.snapshot camera_mode_switch to 0 when needed, camera_photo_take No media id; rejected BUSY while recording ⚠
camera.mode.set camera_mode_switch (0 photo, 1 video, 2 low light, 3 panorama) Used internally; not dispatched by the reference gateway
camera.focus.*, camera.imaging.set, camera.thermal.set camera_focus_mode_set, camera_focus_value_set, camera_point_focus_action, camera_exposure_mode_set, camera_exposure_set, ir_metering_* Not implemented

Before a vehicle.* command the gateway sends flight_authority_grab, and before a camera.* command payload_authority_grab (payload_index), unless the authority is already the cloud's (A.4.5).

services_reply and drc/up data.result map as follows (codes from the Cloud API error code table). No reply within the timeout → REJECTED / LINK_LOST.

UNIVIP DJI result
ACCEPTED 0; live_start_push also 513003 (already streaming)
REJECTED / PILOT_OVERRIDE 316008, 316023, 321778, 327012
REJECTED / LINK_LOST 325012, 327003, 336006, 514120
REJECTED / FLIGHT_STATE 314000, 316022, 316024, 321777, 337025
REJECTED / INVALID_PARAMETERS 325001, 337026, 337027, 513013, 513014, 514171
REJECTED / PAYLOAD_UNAVAILABLE 513002
REJECTED / GEOFENCE 336513, 336515, 337537, 337539, 337546
REJECTED / OUT_OF_LIMITS 327014, 336514, 336516, 336517, 337030, 337538, 337540, 337541
REJECTED / SAFETY 316009, 316026, 319026, 321769, 321772, 336002, 336003, 336017, 337543, 337544, 514103, 514121, 514134, 514135, 514137, 514173
REJECTED / BUSY 319001, 319016, 319025, 319030, 319036–319038, 325005–325011, 327002, 337029, 514116, 514145–514148, 514150, 514170
ACCEPTED then FAILED / INTERNAL any other non-zero code

327003 and 327012 share one description in the error code table; the meanings used here (aircraft offline, authority locked) come from the reference server's control error enumeration ⚠.

A.4.4 EventsLink to the section A.4.4 Events

DJI UNIVIP
hms (the complete active list each time; an alarm missing from the next list is resolved): level 1 → WARNING, 2 → ERROR; level 0 (notification) is not a fault ⚠ Aircraft entries (device_type domain 0) → health.faults[] → vehicle.health.fault: the Part 08 §8.5 code is classified from the alarm text looked up in DJI's hms.json (fpv_tip_{code}, fpv_tip_{code}_in_the_sky), else OTHER ⚠ (J11). Dock entries (domain 3, dock_tip_{code}) → vehicle.dock.fault with DJI's code
mode_code changes to 9 vehicle.navigation.returning_home reason: CLIENT when the gateway sent return_home within 10 s; else mode_code_reason 1, 2, 3, 17 → LOW_BATTERY, 4 → PILOT, 6 → LINK_LOST; else a recent joystick_invalid_notify (0 LINK_LOST, 1 and 2 LOW_BATTERY, 3 GEOFENCE, 4 PILOT) or flyto exit result (338049 PILOT, 338051 and 338052 LOW_BATTERY, 336006 LINK_LOST); else OTHER ⚠ strong wind (23), crewed aircraft nearby (15), obstacles (22), hardware fault (19) have no UNIVIP reason (J9)
fly_to_point_progress, takeoff_to_point_progress (status, remaining_distance, remaining_time, result) Tracks whether a flyto is in progress; the gateway reports its own progress
return_home_info (planned path) — ⚠ no UNIVIP member for a planned path
joystick_invalid_notify reason 4 (remote controller takeover) vehicle.control.pilot_override through control_source (A.4.5)
drc_status_notify DRC link state (internal)
cover_state changes vehicle.dock.state_changed
update_topo without the aircraft Aircraft powered off: IDLE/DOCKED in the dock, otherwise vehicle.link.lost
need_reply: 1 events_reply with data.result 0

A.4.5 Control authority and leasesLink to the section A.4.5 Control authority and leases

DJI arbitrates two authorities per aircraft: flight control and payload control (per payload_index). The holder is reported as control_source: A and B for the two controllers (in a dock deployment the dock's cloud link and an on-site remote controller), a browser-generated UUID in the Pilot 2 cloud case, empty when unassigned. UNIVIP leases (Part 11 §11.3) live in the gateway; they are not DJI authority.

  • Acquiring a UNIVIP lease sends nothing to the dock. The gateway grabs flight or payload authority lazily, right before the first command that needs it, and again after it was lost.
  • control_source in the configured pilot sources (default B) → pilot_override: true: vehicle.* commands are rejected with PILOT_OVERRIDE, running movement fails with PILOT_OVERRIDE, and the lease stays ACTIVE (Part 11 §11.3.3). The gateway never grabs authority from the pilot.
  • Payload authority held by the pilot → camera.* rejected with PILOT_OVERRIDE ⚠ without the camera-scoped vehicle.control.pilot_override Part 10 §10.8 asks for, because telemetry has one vehicle-level flag (J5).
  • 327012 (authority locked on the controller) → PILOT_OVERRIDE.
  • ⚠ Authority is per dock link: every cloud client of the same dock shares A, so a UNIVIP lease cannot exclude a non-UNIVIP cloud user (J5).

A.4.6 Live videoLink to the section A.4.6 Live video

live_start_push (url_type, url, video_id = {aircraft_sn}/{payload_index}/{video_index}, video_quality) makes the dock push the stream: url_type 1 RTMP, 4 WebRTC via WHIP, 3 GB28181, 0 Agora (dock documents do not offer RTSP). The UNIVIP media plane reads the stream back from the media server that receives it (for example RTMP into MediaMTX or SRS, read as rtsp://… or rtmp://…, which FFmpeg opens) and serves it over WebRTC (Part 09). video_index comes from the dock's live_capacity; live_status reports the push; live_stop_push ends it. video_quality 0 is adaptive, 1–4 fix 960×540 at 512 kbps up to 1920×1080 at 3 Mbps, so a UNIVIP stream descriptor can only state the configured ceiling ⚠. The documented API carries no capture timestamp, so frame-to-telemetry alignment (Part 09 §9.5) can only use arrival time at the gateway ⚠ (J12).

A.4.7 Identity and payloadsLink to the section A.4.7 Identity and payloads

DJI UNIVIP
update_topo type/sub_type of dock and sub-device (Product Support table: 1-0 DJI Dock, 2-0 DJI Dock 2; 67-0/1 Matrice 30/30T, 91-0/1 Matrice 3D/3TD) device.manufacturer DJI, device.model e.g. "Matrice 3D (DJI Dock 2)"
aircraft serial number (update_topo sub_devices[].sn, dock sub_device.device_sn) device.serial_number
aircraft state firmware_version device.firmware_version
camera payload_index (52-0-0 M30, 53-0-0 M30T, 80-0-0 M3D, 81-0-0 M3TD) one payload, cam-1 by default; FPV (39-0-7), vision assist (176-0-0) and dock cameras (165-0-7) are not mapped ⚠ wide, zoom and IR lenses of one camera share the payload (J7)

Generated from spec/annex-a-mappings.md.