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.
ColumnTypeDefaultRole
lat, lng, altitudeFloat (NOT NULL)Geographic position; become x_lat / y_long / z_alt
typeEnum base / fly / safepointSemantic label (UI only, not sent to MAVLink)
labelString(255)Human-readable name
sequenceInteger (NOT NULL)Upload order key — points are always loaded ORDER BY sequence ASC
frameInteger3MAVLink frame (0=GLOBAL absolute alt, 3=GLOBAL_RELATIVE_ALT)
commandInteger16MAVLink command (see table below)
is_currentBooleanFalseMAVLink “current waypoint” flag
autocontinueBooleanTrueContinue to next waypoint automatically
param1-param4Float0.0Command-specific parameters
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.
CommandConstantMeaning
16MAV_CMD_NAV_WAYPOINTFly to point (default for stored waypoints)
22MAV_CMD_NAV_TAKEOFFTake off (auto-prepended, see below)
21MAV_CMD_NAV_LANDLand
20MAV_CMD_NAV_RETURN_TO_LAUNCHRTL
178MAV_CMD_DO_CHANGE_SPEEDSpeed change
FrameMeaning
0GLOBAL (absolute MSL altitude)
3GLOBAL_RELATIVE_ALT (altitude relative to home)
Each row emits exactly one rosbridge waypoint dict via MissionPoint.to_mavlink_waypoint() (src/models/mission_point.py:59):
src/models/mission_point.py
def to_mavlink_waypoint(self):
    """Convert to MAVLink waypoint format for upload to drone"""
    return {
        "frame": self.frame or 3,
        "command": self.command or 16,
        "is_current": self.is_current if self.is_current is not None else False,
        "autocontinue": self.autocontinue if self.autocontinue is not None else True,
        "param1": float(self.param1 or 0.0),
        "param2": float(self.param2 or 0.0),
        "param3": float(self.param3 or 0.0),
        "param4": float(self.param4 or 0.0),
        "x_lat": float(self.lat),
        "y_long": float(self.lng),
        "z_alt": float(self.altitude),
    }
The mapping is a straight rename plus float coercion:
ORM columnMAVLink key
latx_lat
lngy_long
altitudez_alt
frame, command, is_current, autocontinue, param1-param4same names
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
if mavlink_waypoints and mavlink_waypoints[0].get("command") != 22:
    takeoff_alt = float(custom_takeoff_altitude) if custom_takeoff_altitude is not None \
        else float(mavlink_waypoints[0].get("z_alt", 10.0))
    takeoff_waypoint = {
        "frame": 3, "command": 22, "is_current": False, "autocontinue": True,
        "param1": 0.0, "param2": 0.0, "param3": 0.0, "param4": 0.0,
        "x_lat": 0.0, "y_long": 0.0, "z_alt": takeoff_alt,  # 0/0 = current position
    }
    mavlink_waypoints.insert(0, takeoff_waypoint)
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:
CommandConstantShapeparam1
5001MAV_CMD_NAV_FENCE_POLYGON_VERTEX_INCLUSIONPolygon vertex (stay inside)vertex count
5002MAV_CMD_NAV_FENCE_POLYGON_VERTEX_EXCLUSIONPolygon vertex (stay outside)vertex count
5003MAV_CMD_NAV_FENCE_CIRCLE_INCLUSIONCircle center (stay inside)radius (m)
5004MAV_CMD_NAV_FENCE_CIRCLE_EXCLUSIONCircle center (stay outside)radius (m)
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
def to_mavlink_fence_item(self):
    return {
        "frame": self.frame or 3,
        "command": self.command,
        "param1": float(self.param1 or 0.0),
        # ... param2-4 ...
        "x_lat": float(self.lat),
        "y_long": float(self.lng),
        "z_alt": 0.0,  # 2D geofence, no altitude
    }

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
PUSH_FENCE = ("/mavros/geofence/push", "mavros_msgs/srv/WaypointPush")
Verification matches push_mission: response.success and wp_transfered == len(fence_items).
MethodRouteBehavior
push_geofencePOST /api/v1/drone/action/push_geofence {drone_id, geofence_id}Upload one geofence’s points as a fence
clear_fencePOST /api/v1/drone/action/clear_fence {drone_id}Push an empty waypoints: [] to PUSH_FENCE
sync_geofencesPOST /api/v1/drone/action/sync_geofences {drone_id}Upload all enabled geofences as one combined fence list
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.
EndpointBodyService method
/drone/action/push_mission{drone_id, mission_id}push_mission (:784)
/drone/action/start_mission{drone_id, takeoff_altitude?}start_mission (:615)
/drone/action/push_geofence{drone_id, geofence_id}push_geofence (:866)
/drone/action/clear_fence{drone_id}clear_fence (:936)
/drone/action/sync_geofences{drone_id}sync_geofences (:959)
Example upload:
curl -X POST https://<gateway-host>/api/v1/drone/action/push_mission \
  -H "Authorization: Bearer <jwt>" \
  -H "Content-Type: application/json" \
  -d '{"drone_id": 42, "mission_id": 7}'

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.