The gateway stores every mission waypoint and geofence vertex in PostgreSQL using MAVLink-native columns (frame, command, param1-4, lat/lng/altitude). When an operator uploads a mission or fence to a drone, the ORM rows are translated 1:1 into MAVROS WaypointPush service items and dispatched over the rosbridge connection. This page documents that ORM → MAVLink translation, the one piece of logic the gateway injects (an auto-prepended TAKEOFF), and the fence command mapping — plus a stale documentation warning you must not trust.
This format is the shared contract between three surfaces: the Dashboard mission planner writes these fields, the gateway persists and translates them here, and the drone’s MAVROS consumes them. See Missions & Geofences API for the CRUD endpoints, Dashboard mission planning for the editor, and MAVLink routing for the on-drone side.

End-to-end upload flow

The MissionPoint model

src/models/mission_point.py carries both a semantic classification (type) used by the UI and the full MAVLink waypoint payload. A single row is one waypoint. mission_id and user_id are both ON DELETE CASCADE FKs. The parent Mission (src/models/mission.py) is just a header (user_id, name, unique on uq_user_name) with a mission_points relationship. Full schema context lives in Database Schema Overview. These are the command codes the fields hold. Only 16 and 22 are produced automatically by the gateway; the rest come from whatever the Dashboard persisted. Each row emits exactly one rosbridge waypoint dict via MissionPoint.to_mavlink_waypoint() (src/models/mission_point.py:59):
src/models/mission_point.py
The mapping is a straight rename plus float coercion:
frame or 3 clobbers frame 0. Because 0 is falsy in Python, a waypoint deliberately stored with frame = 0 (GLOBAL / absolute-MSL altitude) is silently uploaded as frame = 3 (relative-to-home). The same falsy-coalescing applies to command or 16, though 0 is not a valid command so it is harmless there. If you ever need absolute-altitude waypoints, this conversion must change to an explicit is None check.

push_mission() — the auto-TAKEOFF prepend

DroneControlService.push_mission() (src/service/drone_control_service.py:784) is the only place the gateway adds logic on top of the stored rows. It is invoked from POST /api/v1/drone/action/push_mission and internally by start_mission().
1

Load & convert

mission_service.get_mission_points(mission_id, user_id) returns rows ordered by sequence; each is mapped through to_mavlink_waypoint().
2

Prepend TAKEOFF if missing

If the list is non-empty and the first command is not 22, a synthetic TAKEOFF is inserted at index 0: frame=3, command=22, x_lat=0.0, y_long=0.0 (ArduPilot takes off from current position when lat/lng are 0). The takeoff altitude is custom_takeoff_altitude if provided, otherwise the first waypoint’s z_alt (falling back to 10.0).
3

Push over rosbridge

Sends a call_service frame to PUSH_MISSION = ("/mavros/mission/push", "mavros_msgs/srv/WaypointPush") with {"start_index": 0, "waypoints": [...]}, waiting up to ROSBRIDGE_SERVICE_TIMEOUT (30s) for a response.
4

Verify transfer count

Requires response["success"] and values["wp_transfered"] == len(mavlink_waypoints); otherwise raises Mission upload incomplete.
5

Record last-uploaded mission

On success, drone_service.update({"id": drone_id, "mission_id": mission_id}) stamps the drone row so it remembers the mission it is carrying.
src/service/drone_control_service.py
wp_transfered is spelled with one r — that is the actual field name in the MAVROS WaypointPush response, not a typo in this codebase. Do not “correct” it.

Relationship to start_mission

POST /api/v1/drone/action/start_mission {drone_id, takeoff_altitude?} calls push_mission() again (re-uploading the drone’s assigned mission), then sets GUIDED mode, arms, and issues a GUIDED takeoff. It does not switch the vehicle to AUTO; the Dashboard monitors relative altitude and performs the AUTO switch. A server-side helper _monitor_takeoff_and_switch_to_auto() (src/service/drone_control_service.py:676) exists for this but is not invoked by start_mission(). See DroneControlService & Rosbridge Dispatch for the command layer.
The CLAUDE.md “Mission Format” description is stale. It claims missions generate a home row, a speed row (command 178), an auto-reversed return path, and a trailing RTL (command 20, frame 2). None of that is implemented in push_mission(). The backend uploads the stored waypoints exactly as-is and only prepends a single TAKEOFF (command 22). Any reversed-path / RTL / speed behavior, if it exists at all, is constructed frontend-side in the mission planner — verify before relying on it.

Geofences: fence commands 5001-5004

Geofences reuse the same MAVLink item shape but with fence commands instead of nav commands. A Geofence (src/models/geofence.py) is type (polygon / circle) × fence_type (inclusion / exclusion, default exclusion) with cascade-deleted GeofencePoint children. src/models/geofence_point.py defines the command mapping: get_fence_command(geofence_type, fence_type) selects the code from the type × fence_type pair. Each vertex/center is a GeofencePoint (lat, lng, sequence, frame default 3, command NOT NULL, param1-param4). GeofencePoint.to_mavlink_fence_item() (src/models/geofence_point.py:66) mirrors to_mavlink_waypoint() but forces z_alt = 0.0 because fences are 2D:
src/models/geofence_point.py

push_geofence() and friends

DroneControlService.push_geofence() (src/service/drone_control_service.py:866) loads the geofence with points, computes the command via get_fence_command(geofence.type, geofence.fence_type or "exclusion"), then overrides every item’s command with that value. For polygons it also overwrites param1 with the total vertex count (len(fence_items)); circles keep the stored radius. It pushes via a dedicated service:
src/utils/mavros_topics.py
Verification matches push_mission: response.success and wp_transfered == len(fence_items).
The MAVLink fence protocol supports multiple polygons/circles in a single fence list, which is why sync_geofences() can concatenate every enabled geofence into one WaypointPush. clear_fence() uses the same push service with a zero-length list.

Endpoint summary

All actions are POST under /api/v1, JWT-protected (@jwt_required()), defined in src/routes/drone_routes.py. Example upload:

Missions & Geofences API

CRUD endpoints that populate these tables.

DroneControlService

How call_service frames reach the drone over rosbridge.

Rosbridge Connection

The send_service_call_with_response transport and timeouts.

Schema Overview

Full column list and cascade rules for these tables.