-
Notifications
You must be signed in to change notification settings - Fork 2
Mission Planning
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.
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.
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.
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.
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).
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 fromatob. -
pathLengthMeters(points): total length along a path. -
pointInRing/pointInPolygon: ray-casting point-in-polygon test with hole support, used for airspace containment.
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, andclearedLegsreport what changed;changedis 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.
The map draws airspace for the mission area:
-
refreshAirspace(actions)insrc/lib/preflight.tscomputes a bounding box around the waypoints and home (padded by 0.1 degrees), callsGET /api/airspace, and caches the zones inairspaceZonesStore. - 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.tsbuilds 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.
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.tsholds 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.
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.
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.