Skip to content

Mission Planning

Judah Paul edited this page Jul 15, 2026 · 16 revisions

The mission planner (/mission-planner) edits a list of waypoints on a 2D map (Leaflet) and a 3D map (MapLibre). On top of the editor it adds path optimization, airspace overlays, and pre-flight safety validation. The waypoint list is a MissionPlanActions map (index to MissionPlanItem), and each item carries a MAVLink command type, lat, lon, and alt.

One map instance serves the whole app: it renders from the layout as a full-viewport background on every page, and pages register screen rects for it to frame (src/lib/map-window.ts, mapWindowStore). Every interactive window edits the plan: double-click adds a waypoint and markers drag. The planner's map cell carries the full chrome (overlay controls, the type toggle), the dashboard mini-map the reduced set, and fullscreen expands the same instance with the live-feed and manual-control docks. Everything outside the active window is dimmed and click-blocked, and the page shell and Controls card are redrawn around the window so the panels keep their surfaces.

The map toggles (view lock, the overlay controls, and the street/satellite/3D selection) persist in sessionStorage (src/lib/session-persisted.ts), so a reload within the browser session restores them. Both maps render the same feature set: mission paths and markers, the vehicle marker, and the airspace, ceiling, obstacle, and traffic overlays, with mission features stacked above the overlay layers. Clicking any feature on either map opens the combined feature modal; the 3D map hit-tests through the MapLibre camera projection so pitch and bearing keep the same pixel tolerance. Spline waypoint legs draw the curved hermite path ArduPilot flies (src/lib/spline-path.ts, a port of AC_WPNav's spline solution with its neighbor-derived tangents and overshoot clamp); when PX4 is connected the legs stay straight, matching the NAV_WAYPOINT substitution on upload.

Mission patterns

src/lib/mission-patterns.ts generates waypoints for five patterns, each appended to the plan through the same seeding path as a double-clicked waypoint. That path brackets the mission automatically: a hidden home slot and a takeoff open it, and a return-to-launch closes it, both added when absent, so new waypoints land between the takeoff and the return and the vehicle comes home instead of holding at the last point.

  • Survey: the planner collects polygon corner clicks on the map (double-click finishes, Escape cancels), then a prompt takes transect spacing, grid angle from north, and altitude, and lays serpentine transects across the area. Each transect's entry and exit come from intersecting the polygon with lines stacked along the grid normal.
  • Orbit: one click sets the center, then a prompt takes radius, waypoint count, altitude, and direction, and rings the center starting north, closing back on the first point.
  • Corridor: clicks trace a path (double-click finishes), then a prompt takes corridor width, lane spacing, and altitude, and lays parallel serpentine lanes offset perpendicular to the path, for a road or shoreline sweep.
  • Search: one click drops the datum (a last-known point), then a prompt takes track spacing, leg count, altitude, and direction, and walks an expanding square whose legs grow by the spacing every two turns.
  • Structure scan: one click sets the center, then a prompt takes radius, waypoints per ring, base altitude, layer count, and height per layer, and stacks orbit rings that spiral up a tower or facade, alternating direction each layer.

They start from the pattern buttons in the plan header (MissionPlan.svelte), which groups the plan-authoring tools (the patterns plus optimize path) beside the flight controls; the in-progress capture previews as yellow markers and a dashed outline. The settings column keeps the mission-library actions: home location, manage, save and load, clear, import, and export.

Gamepad flight

The dashboard Controls card carries a gamepad toggle in its top-right corner, and the fullscreen manual-control dock carries the same toggle; both drive one shared session (src/lib/gamepad-session.ts). With no gamepad visible to the browser, the toggle opens a connect dialog that walks the USB-or-Bluetooth pairing and press-any-button steps, then previews the detected pad's live sticks and buttons before flight starts.

While enabled, src/lib/gamepad-control.ts maps the first connected standard-layout gamepad onto MAVLink MANUAL_CONTROL axes (Mode 2: left stick throttle and yaw, right stick pitch and roll, 8% deadzone rescaled) and streams frames at 20 Hz through /api/mavlink/manual_control. Pitch, roll, and yaw span -1000..1000; thrust spans 0..1000 with 500 at stick center, because ArduPilot maps it onto the throttle channel's 0..1000 override (and drops frames with negative thrust) while PX4 computes throttle as (z / 1000) * 2 - 1, so a centered stick hovers on both.

The session manages the mode transitions around the stream. Enabling it in flight switches the vehicle to its stick-flying mode once the autopilot is receiving input: Position mode on PX4, Loiter on ArduPilot, both of which hold position on centered sticks; a running mission pauses with the mode change and resumes from Start/Resume Mission. When the stream ends, on toggle or gamepad disconnect, the session immediately hands the vehicle to an autonomous hold (Hold on PX4, GUIDED position hold on ArduPilot) so it never sits in a pilot mode waiting for input that stopped. If the app's own link to the vehicle dies instead, the autopilot's RC and GCS loss failsafes govern; on ArduPilot, MANUAL_CONTROL rides RC overrides, which expire after RC_OVERRIDE_TIME (3 s default), so a joystick-only vehicle with no RC receiver should have its radio failsafe configured deliberately.

Autopilot compatibility

The stored plan is autopilot-neutral: each item names a MAVLink command with no target stack baked in. normalizeMission in src/lib/mission-commands.ts resolves the plan into wire-ready items for the connected autopilot at upload time. ArduPilot runs the full command table. PX4 rewrites commands it lacks to an equivalent (NAV_SPLINE_WAYPOINT becomes NAV_WAYPOINT) and skips the ones it cannot run (NAV_GUIDED_ENABLE, CONDITION_*, DO_WINCH, and similar), raising one notification per change. The saved plan stays the same, so a mission built once uploads to either stack.

Importing missions

src/lib/mission-import.ts reads six formats into the MissionPlanActions model: QGroundControl .plan JSON, the Mission Planner QGC WPL 110 .waypoints/.txt format, the app's own exported JSON, a Google Earth .kml or .kmz, and a plain coordinate .csv. MAVLink command numbers map back to names through the same table used for upload, and QGC ComplexItems such as surveys are skipped. KML pulls lon,lat,alt tuples from every <coordinates> block or <gx:Track>; a .kmz is unzipped from its ZIP central directory and inflated with the platform DecompressionStream before the KML reader runs; CSV maps latitude, longitude, and altitude columns by header name or reads a headerless file in that column order. The Import Mission button in mission settings accepts any of these, and readMissionFile routes the picked file to the right reader (bytes for .kmz, text otherwise).

Geo helpers

src/lib/geo.ts holds the shared geometry, all pure functions:

  • haversineMeters(a, b): great-circle distance between two points.
  • bearingDegrees(a, b): initial bearing from a to b.
  • pathLengthMeters(points): total length along a path.
  • pointInRing / pointInPolygon: ray-casting point-in-polygon test with hole support, used for airspace containment.

Path optimization

optimizeMissionPath(actions, airspace, obstacles) in src/lib/path-planning.ts routes the mission clear of hazards while keeping the waypoint order, and returns an OptimizeResult (actions, addedWaypoints, movedWaypoints, clearedLegs, changed). The Optimize Path button in mission settings fetches fresh airspace and obstacles for the mission area, then applies it.

  • The waypoint order is preserved; nothing is reordered.
  • Vertical hazards are FAA obstacles (Digital Obstacle File) and building footprints with heights from OpenStreetMap (GET /api/buildings, Overpass). A leg that would strike one is raised to clear its top plus a margin when the altitude ceiling allows, and routed around with detour waypoints when the hazard is too tall to climb over.
  • Restricted airspace is a legal boundary, so it is always routed around, and a movement waypoint sitting inside restricted airspace is projected just outside it. Takeoff, RTL, land, and loiter keep their position, and landing commands keep their altitude.
  • addedWaypoints, movedWaypoints, raisedWaypoints, and clearedLegs report what changed; changed is false when the route already clears every hazard.

The map overlays refetch for the visible area as the operator pans (debounced, skipped when zoomed far out). The airspace, hazard, and building endpoints sit behind an in-memory TTL cache keyed by rounded bounding box, so panning reuses recent results instead of hammering the upstream FAA and Overpass APIs.

The four air-hazard overlays (airspace, LAANC ceilings, obstacles, and live traffic) start off and enable themselves once the connected vehicle reports an air vehicle type; for rovers, boats, and submarines they stay off but remain toggleable from the map controls. A toggle the operator sets by hand wins over the auto-enable and is remembered for the browser session. Off the planner the overlay layers hide while the toggle states hold, and returning to the planner redraws and refetches whatever is enabled.

Airspace overlays

The map draws airspace for the mission area:

  • refreshAirspace(actions) in src/lib/preflight.ts computes a bounding box around the waypoints and home (padded by 0.1 degrees), calls GET /api/airspace, and caches the zones in airspaceZonesStore.
  • Both the 2D and 3D maps render restricted zones (red) and controlled zones (amber) as filled polygons; a map control toggles the layer (showAirspaceStore). Clicking a zone shows its name, class, altitude band, and operational implication. src/lib/airspace.ts builds the popup HTML and holds the shared colors so both maps read the same zones.
  • The endpoint sources zones from OpenAIP worldwide when a key is set, and falls back to the FAA's keyless public layers (US) otherwise. With neither reachable the zone list is empty and the overlay is blank. See Configuration.

LAANC ceilings and obstacles

GET /api/hazards pulls two more keyless FAA layers for the mission's bounding box, cached in ceilingCellsStore and obstaclesStore by refreshHazards in src/lib/preflight.ts and rendered as toggleable overlays on both maps:

  • The UAS Facility Map grid: each square-mile cell colored by its pre-approved LAANC ceiling, with a popup stating the ceiling in feet and meters and what it authorizes. src/lib/hazards.ts holds the types, colors, and popup builders.
  • Digital Obstacle File points (towers, poles, stacks) colored by height, with type and AGL/MSL heights in the popup. When an area holds more obstacles than one query returns, the tallest are kept.

Live air traffic

GET /api/traffic returns nearby aircraft for a bounding box from keyless community ADS-B feeds (adsb.lol, then adsb.fi), cached per area for five seconds. While the plane-icon toggle is on, the layer polls every five seconds and merges ADSB_VEHICLE reports from the vehicle's own receiver (src/stores/trafficStore.ts); contacts expire fifteen seconds after their last update. Markers on both maps rotate to each aircraft's track and open the combined feature modal with callsign, altitude, speed, track, and source.

Pre-flight safety checks

Starting a mission calls preflightCheck(actions), which resolves true only when it is safe to launch. It validates every positional waypoint with validateMission against SafetyLimits:

Limit Default Check
maxAltitudeM 120 Waypoint altitude above the ceiling is a blocking error.
minAltitudeM 0 Waypoint altitude below the floor is a blocking error.
geofenceRadiusM 1000 Waypoint farther than this from home is a blocking error.
airspace fetched A waypoint in restricted airspace is a blocking error; controlled airspace is a warning. A leg that crosses restricted airspace is blocked even when both endpoints sit outside it.
LAANC ceiling fetched A waypoint above its grid square's ceiling, or inside a 0 ft square, is a warning naming the ceiling and airport.
obstacles fetched A leg passing within 100 m of an obstacle taller than the leg floor minus a 10 m clearance is a warning naming the obstacle and the altitude that clears it.

Home defaults to the live vehicle location when the limits do not set it, and the limits live in safetyLimitsStore. validateMission returns a SafetyViolation[]; hasBlockingViolation reports whether any is a hard error. formatViolations renders them for the modal, collapsing repeated per-waypoint entries that share a predicate (every waypoint inside one airspace zone, or in a 0 ft grid) into a single line with the waypoint range, so a long mission through one zone reads as Waypoints 7-15 inside controlled airspace: ATLANTA CLASS E4 rather than one line each.

Behavior at launch:

  • No violations: the mission starts.
  • Blocking violations (error): a modal lists them and the start is cancelled.
  • Warnings only: a modal lists them and asks the operator to confirm before starting.

These checks run before commands are sent. They are an advisory guard on top of the autopilot's own geofence and failsafes, not a replacement for them.

Clone this wiki locally