Skip to content

Missions: Add geo fencing#2671

Open
ArturoManzoli wants to merge 14 commits into
bluerobotics:masterfrom
ArturoManzoli:add-geo-fencing
Open

Missions: Add geo fencing#2671
ArturoManzoli wants to merge 14 commits into
bluerobotics:masterfrom
ArturoManzoli:add-geo-fencing

Conversation

@ArturoManzoli

@ArturoManzoli ArturoManzoli commented May 7, 2026

Copy link
Copy Markdown
Contributor

This PR was split into thee others to ease the review:

#2807 - Add geofence vehicle sync and plan types
#2808 - Mission Planning: Add geofence planning editor
#2809 - Add live geofence map overlay

Replace the per-key "if undefined ..." chain in onBeforeMount with a single
spread merge of a defaultOptions object over the persisted options. Aligns
the Map widget with the convention documented in AGENTS.md (and used in
Plotter.vue) so future option additions are a one-liner instead of another
conditional branch.

Signed-off-by: Arturo Manzoli <arturomanzoli@gmail.com>
Replace the three independently absolute-positioned bottom-right buttons
(Edit mission, Download tiles, Center-on-target speed dial) with a single
flex-row container (`map-bottom-buttons`, `gap: 10px`). Lays the groundwork
for additional map overlay buttons to slot in without reshuffling
right-offsets, and keeps the existing Leaflet scale offset untouched.

Signed-off-by: Arturo Manzoli <arturomanzoli@gmail.com>
… row container

Wrap the three independently absolute-positioned bottom-right buttons
(Switch to Flight mode, Download tiles, Center-on-target speed dial) into
a single flex-row container (`planning-bottom-buttons`, `gap: 10px`),
mirroring the equivalent change to the Map widget. The Leaflet scale
offset is updated from 293px to 278px to track the new container layout.

Signed-off-by: Arturo Manzoli <arturomanzoli@gmail.com>
Adds the runtime types backing the new geofencing flow: FenceLatLng,
FencePolygon (with inclusion flag and vertex list), FenceCircle,
BreachReturnPoint, the Cockpit-level GeoFencePlan, the on-disk
CockpitFencePlanFile (.cfp) envelope, and the MavlinkPlanFile envelope
that lets fence (and mission) plans round-trip with other ground stations
through the de-facto MAVLink-ecosystem .plan JSON format. Type guards
(instanceOfGeoFencePlan, instanceOfCockpitFencePlanFile,
instanceOfMavlinkPlanFile) only inspect the outer discriminator + version
so deeper validation stays at the call site that knows the schema.

Signed-off-by: Arturo Manzoli <arturomanzoli@gmail.com>
@ArturoManzoli ArturoManzoli marked this pull request as draft May 7, 2026 17:26
On vehicle bring-up, send MAV_CMD_REQUEST_AUTOPILOT_CAPABILITIES and
cache the bitmask received via the AUTOPILOT_VERSION message. Exposes
`capabilities()` / `hasCapability(bit)` accessors, and a new
`onCapabilities` signal so consumers can react when the bitmask first
arrives. Failures are logged at warn level and gracefully fall back to
"optimistic" capability reporting (consumers default to enabling
features when no AUTOPILOT_VERSION has been seen yet).

Signed-off-by: Arturo Manzoli <arturomanzoli@gmail.com>
Adds two helpers that turn the existing PARAM_REQUEST_READ / PARAM_VALUE
round-trip into something callers can actually \`await\`:

- \`requestParameter(name)\`: fires a single PARAM_REQUEST_READ for the
  given parameter id; the reply still bubbles up through \`onParameter\`
  for code that wants to keep listening passively.
- \`requestParameterValue(name, timeoutMs)\`: subscribes a one-shot listener
  that resolves with the value as soon as the matching PARAM_VALUE arrives
  (or undefined on timeout). Both the listener and the timeout handle are
  cleared as soon as the promise settles to avoid stray callbacks.

Signed-off-by: Arturo Manzoli <arturomanzoli@gmail.com>
Wires geofence support into the existing MAVLink mission micro-service:

- geofence-conversion.ts: pure helpers that translate between a Cockpit
  GeoFencePlan and a flat list of MissionItemInt[]. Polygons are emitted
  vertex-per-item with a shared param1=vertex_count; circles use param1
  for the radius; the optional breach return point goes last as a
  MAV_FRAME_GLOBAL_RELATIVE_ALT item. The reverse path detects bad/incomplete
  polygon item streams from the autopilot and surfaces a descriptive error.
- vehicle.ts: extracts _fetchMissionItems / _uploadMissionItems from the
  mission code so fence and mission paths share the handshake, then layers
  uploadFence / fetchFence / clearFence on top using
  MAV_MISSION_TYPE_FENCE. uploadFence also calls
  _ensureArduPilotPolygonFenceTypeBit to set the Polygon bit (4) of
  FENCE_TYPE on ArduPilot — without it the autopilot silently ignores the
  uploaded polygons/circles. Other FENCE_TYPE bits are preserved.

Signed-off-by: Arturo Manzoli <arturomanzoli@gmail.com>
Surfaces the new MAVLink capability and fence APIs through the existing
main-vehicle Pinia store so consumers don't need to reach into the
underlying MAVLinkVehicle instance:

- \`capabilities\` ref + \`onCapabilities\` subscription, kept in sync as
  AUTOPILOT_VERSION arrives.
- \`uploadFence\` / \`fetchFence\` / \`clearFence\` proxy actions, mirroring
  the existing mission helpers (and emitting the same "Geofence deleted
  from vehicle" snackbar on clear).
- \`requestParameter\` proxy so other stores (notably the upcoming
  geo-fence store) can pull individual parameter values without
  duplicating the MAVLink boilerplate.

Signed-off-by: Arturo Manzoli <arturomanzoli@gmail.com>
Pinia store that owns all editor-side geofencing state:

- Reactive polygons, circles and breach-return point with
  \`cockpit-draft-fence\` (in-progress) and \`cockpit-vehicle-fence\`
  (last uploaded) persistence so the user doesn't lose work across
  reloads.
- Plan import/export between Cockpit's runtime shape, the .cfp envelope
  and the de-facto MAVLink-ecosystem .plan file.
- Vehicle sync: \`uploadToVehicle\` (auto-enables FENCE_ENABLE and
  FENCE_AUTOENABLE on ArduPilot once the upload completes),
  \`downloadFromVehicle\`, \`clearOnVehicle\`, plus \`setFenceEnabled\` /
  \`setFenceAutoEnable\` that talk to the new MAVLink helpers.
- \`FENCE_ENABLE\` polling watch (3 s) tied to ArduPilot + a loaded plan
  so the Map widget toggle reflects autopilot-side flips (e.g.
  takeoff-time activation) the autopilot doesn't broadcast.
- Capability gate via \`isFenceSupported\` (\`MISSION_FENCE\` bit) and a
  \`detectMissionBreaches\` helper that flags mission waypoints which
  fall outside an inclusion fence or inside an exclusion fence so the
  UI can warn before mission upload.

Signed-off-by: Arturo Manzoli <arturomanzoli@gmail.com>
Leaflet layer responsible for rendering all geofence shapes — both during
interactive editing in MissionPlanningView and as the read-only overlay
on the Map widget.

- Polygons render with a 45-degree striped SVG pattern for exclusion
  zones and a translucent blue fill for inclusion zones, with a stripe
  pattern that's dimmed by 50% when \`readonly\` so the live overlay
  stays visually quieter than the editor.
- Vertex / center / "+" handles drive polygon shape edits in editor mode
  (move cursor on draggable handles, copy cursor on midpoint add-vertex
  handles); circles get a center handle plus a draggable edge handle
  with a live distance tooltip. Breach-return drag reads its altitude
  back from the store on dragend so altitude edits made between marker
  creation and drag aren't overwritten.
- \`L.Layer | null\` types kept loose where Leaflet's typings would
  otherwise fight us.

Signed-off-by: Arturo Manzoli <arturomanzoli@gmail.com>
Collapsed-by-default ExpansiblePanel that exposes the curated set of
autopilot-side fence parameters. ArduPilot lists FENCE_ACTION /
FENCE_ALT_MAX / FENCE_ALT_MIN / FENCE_MARGIN / FENCE_AUTOENABLE; PX4
lists GF_ACTION / GF_MAX_HOR_DIST / GF_MAX_VER_DIST / GF_PREDICT.
Per-parameter info icons open a click-triggered tooltip with the full
description so we don't drown the panel in inline text. The PARAM_VALUE
listener is captured in a named handler and removed in onBeforeUnmount
to avoid accumulating listeners on remount; failed param writes surface
as a snackbar instead of failing silently.

Signed-off-by: Arturo Manzoli <arturomanzoli@gmail.com>
Sidebar component that drives all editor-side geofence interactions:

- Compact "Add fence" row with polygon and circle buttons that match the
  Add survey / Add simple path styling; new fences live in expansible
  panels and the cards expose a clickable inclusion/exclusion tag plus a
  scaled-down on/off switch.
- Breach-return section with an "Add breach return point" button (info
  icon on the right) and an "Auto enable fence on takeoff" switch
  (defaults on; flips FENCE_AUTOENABLE on ArduPilot).
- Vehicle controls: upload to vehicle, clear on vehicle, and import /
  export of both Cockpit-native .cfp files and the de-facto MAVLink
  ecosystem .plan files. clearOnVehicle and parameter writes are wrapped
  in try/catch so failures surface as a dialog or snackbar instead of
  failing silently.
- Embeds the GeoFenceParametersPanel for autopilot-side fence parameters.

Signed-off-by: Arturo Manzoli <arturomanzoli@gmail.com>
Wires the geofencing flow into the planning view:

- A compound "Mission / GeoFence" planning-mode switcher; the sidebar
  swaps between mission editing controls and the new GeoFenceEditor.
- Cruise-speed input + "Add survey" / "Add simple path" buttons gated
  on planningMode === "mission" so they disappear in geofence mode.
- Click-to-draw flow for polygon and circle fences, mirroring the
  survey-polygon mechanics (crosshair cursor, live edge measurement,
  add-vertex midpoints, vertex dragging) plus a click-to-set-center /
  drag-to-set-radius flow for circles. Confirm/delete buttons hover
  near the shape, no inline distance fields.
- Pre-upload safety net: confirmMissionFenceBreachIfNeeded runs
  fenceStore.detectMissionBreaches against the planned waypoints and,
  if any waypoint is outside an inclusion fence or inside an exclusion
  fence, prompts the user with "Back to mission planning" /
  "Upload to vehicle anyway" before letting the upload proceed.

Signed-off-by: Arturo Manzoli <arturomanzoli@gmail.com>
- Renders the last uploaded fence read-only on the map widget via
  GeoFenceMapLayer. The overlay is mounted only while enforcement is on
  (and a fence is loaded) so toggling enforcement off also hides the
  fence polygons/circles and the breach-return marker; turning it back
  on re-renders them automatically.
- Adds a fence enforcement speed-dial to the bottom-right buttons row
  (visible only on ArduPilot once a fence is loaded). The shield
  activator opens the dial on click and turns the same orange as
  exclusion zones whenever the fence is active; double-clicking it
  toggles FENCE_ENABLE directly through fenceStore.setFenceEnabled
  without leaving the map. The dial exposes a toggle item and a
  reload item that triggers a fresh fence download from the vehicle.
- Adjusts the Leaflet scale offset from 293px to 319px to track the
  extra width the fence speed-dial adds to the bottom-right flex row.

Signed-off-by: Arturo Manzoli <arturomanzoli@gmail.com>
@ArturoManzoli ArturoManzoli force-pushed the add-geo-fencing branch 2 times, most recently from 0fde676 to 1015db7 Compare May 7, 2026 18:09
@ArturoManzoli ArturoManzoli marked this pull request as ready for review May 13, 2026 14:40
@github-actions

Copy link
Copy Markdown

Automated PR Review (Claude)

0. Summary

Verdict: MINOR SUGGESTIONS

Minor items to address: 1.1, 1.2, 1.3, 4.1, 4.2, 6.1, 6.2.

This PR adds a comprehensive geofence planning module to Cockpit's Mission Planning view. It introduces polygon and circle fence shapes (inclusion/exclusion), a breach return point, fence parameter management (ArduPilot + PX4), MAVLink upload/download via the fence mission micro-service, live fence overlay on the flight Map widget, fence enforcement toggling, waypoint-vs-fence breach checking before mission upload, and .cfp / .plan file import/export. The implementation is well-structured across a new Pinia store (geoFence.ts), MAVLink conversion utilities, three new Vue components, and updates to the existing Map widget and MissionPlanningView.


1. Correctness & Implementation Bugs

1.1 (minor) geoFence.ts — The fenceEnablePollHandle interval (setInterval(refreshFenceEnabled, 3000)) is created inside a watch() in a Pinia store (composition API). Pinia stores are singletons that live for the app's lifetime, so the watcher self-manages via the [isArduPilot, lastUploadedPlan, isVehicleOnline] watch deps, which is correct. However, if the store itself is ever $dispose()-d (e.g. in tests), the interval leaks. Consider exposing a teardown or using tryOnScopeDispose from @vueuse/core (already a dependency) for safety.

1.2 (minor) GeoFenceParametersPanel.vue — The onParamValueMessage listener is added in onMounted and removed in onBeforeUnmount. But vehicleStore.mainVehicle may be null at mount time (no vehicle connected yet). If it becomes available later, the listener is never attached. The existing watch(() => vehicleStore.firmwareType, refreshAllParams) only refreshes params but doesn't re-bind the listener. Consider watching vehicleStore.mainVehicle to attach/detach the listener, mirroring the pattern in geoFence.ts (watch(() => mainVehicleStore.mainVehicle, ...)).

1.3 (minor) GeoFenceMapLayer.vue — The heavy watch source (lines ~1273-1292 in the diff) serialises every polygon vertex to a string on each reactivity tick to detect changes:

sourcePolygons().map((p) => ({
  id: p.id,
  inclusion: p.inclusion,
  vertices: p.vertices.map((v) => `${v[0]},${v[1]}`),
})),

With { deep: true }, this creates a new array of objects every evaluation, which the deep watcher then diffs. This will trigger syncLayers() on every single drag step of every vertex. It works, but during vertex drag events (which fire on every mousemove) this causes many redundant layer re-renders. The deep: true is also redundant here since the getter already returns new objects each call. Consider throttling or debouncing syncLayers, or at minimum removing { deep: true } since the getter already creates fresh references.

1.4 (nit) geofence-conversion.ts:itemCoordinates — Division by 1e7 can introduce floating-point precision artifacts. This is the standard MAVLink approach and acceptable, but worth noting that round-trip encode→decode won't be bit-identical.


2. AGENTS.md Adherence

2.1 (nit) Comment policy (explain "why" not "what"): Several new comments describe "what" rather than "why", e.g. // Fence polygon drawing — mirrors the survey-polygon drawing infrastructure (MissionPlanningView.vue). This is borderline acceptable as it explains the rationale for mirroring the pattern, but some comments like // Read-only renders (e.g. the live overlay...) get every fence-related opacity halved in GeoFenceMapLayer.vue are more "what" than "why". Per AGENTS.md rule: "No new comments unless explaining 'why', never 'what'".

2.2 (pass) Existing dependencies: @turf/turf, uuid, leaflet, date-fns, file-saver, pinia — all already in package.json. No new dependencies added. ✓

2.3 (pass) cockpit- prefix for local storage keys: cockpit-draft-fence, cockpit-vehicle-fence — both correctly prefixed. ✓

2.4 (pass) Widget options default-merging pattern: The Map widget's onBeforeMount was refactored from individual if (x === undefined) checks to the recommended { ...defaultOptions, ...widget.value.options } merge pattern. ✓ Good improvement.

2.5 (pass) JSDoc completeness: All new public functions and interfaces have JSDoc with typed @param and @returns. No empty entries found. ✓

2.6 (pass) Optional chaining: Used consistently throughout (e.g. mainVehicleStore.mainVehicle?.onIncomingMAVLinkMessage, polygonVertexMarkers.get(id)?.forEach(...), etc.). ✓


3. Security

3.1 (pass) No obfuscated or intentionally unreadable code detected.

3.2 (pass) No suspicious base64/hex/long-encoded blobs, binary-like strings, or unusually large encoded constants.

3.3 (pass) No hidden Unicode, zero-width characters, right-to-left overrides, or homoglyph attacks detected. All identifiers use standard ASCII.

3.4 (pass) No unexpected network calls. All communication goes through the existing MAVLink message pipeline (sendMavlinkMessage). No new fetch, XMLHttpRequest, or WebSocket connections to unknown hosts.

3.5 (pass) No changes to build scripts, postinstall hooks, CI workflows, Dockerfiles, or Electron main-process code.

3.6 (pass) No new environment variables, tokens, credentials, weakened CORS/CSP, eval, Function(), or v-html usage. The template uses v-if/v-show and string interpolation only.

3.7 (pass) No new dependencies added to package.json.

3.8 (pass) No patterns suggesting malicious behavior. File I/O is limited to user-initiated file picker (<input type="file">) for import and file-saver for export — both standard patterns already used in the codebase.


4. Performance

4.1 (minor) geoFence.tspersistDraft() calls exportPlan() which does a deep clone of all polygons/circles. This is called on every updatePolygon / updateCircle, which fires on every mousemove during drag. For polygons with many vertices, this creates significant GC pressure. Consider debouncing persistDraft() (e.g. 300ms trailing) so it only writes to storage after the user stops dragging.

4.2 (minor) GeoFenceMapLayer.vue — As noted in 1.3, the watcher's getter creates new arrays/objects on every tick. Combined with vertex drag (which calls updatePolygon → marks reactive state dirty → triggers the watcher), syncLayers() runs on every mousemove during drag. Each syncLayers call iterates all polygons/circles and recreates all vertex/midpoint/center handles for the interactive shape. A throttle (e.g. requestAnimationFrame gating) would improve responsiveness with many shapes.

4.3 (nit) geoFence.tsimport * as turf from '@turf/turf' imports the entire Turf.js library. The code only uses turf.polygon, turf.point, turf.booleanPointInPolygon, and turf.distance. Tree-shaking should handle this at build time since @turf/turf re-exports from subpackages, but if bundle size is a concern, targeted imports (import { polygon, point, booleanPointInPolygon, distance } from '@turf/turf') make the intent clearer.


5. UI / UX

5.1 (nit) The fence speed-dial on the Map widget is gated by fenceStore.lastUploadedPlan && fenceStore.isArduPilot. This means PX4 users never see the fence overlay button on the flight map, even though geofence upload works for PX4. The fence enforcement toggle (FENCE_ENABLE) is ArduPilot-specific, but the overlay could still be useful for PX4.

5.2 (nit) The "Finish polygon fence" confirm button uses fenceConfirmButtonStyle which is computed from the vertices' screen positions. On small screens or when the polygon is near a viewport edge, the button could overflow. The pickBestPosition helper (reused from the survey flow) handles this, so this is likely fine, but worth verifying on narrow viewports.

5.3 (nit) In the sidebar, when planningMode === 'geofence', the mission section is completely hidden via multiple v-if="planningMode === 'mission'" guards. If a user switches to geofence mode while survey polygon drawing is active, the watch(planningMode) watcher cancels fence drawings but doesn't cancel survey drawing (isCreatingSurvey, isDrawingSurveyPolygon). This could leave orphaned survey polygon vertices on the map.


6. Code Quality & Style

6.1 (minor) GeoFenceEditor.vueonTogglePolygonInteractive and onToggleCircleInteractive have identical implementations:

const onTogglePolygonInteractive = (id: string): void => {
  fenceStore.setInteractive(fenceStore.interactiveShapeId === id ? undefined : id)
}
const onToggleCircleInteractive = (id: string): void => {
  fenceStore.setInteractive(fenceStore.interactiveShapeId === id ? undefined : id)
}

These could be a single onToggleInteractive(id) function to reduce duplication.

6.2 (minor) GeoFenceMapLayer.vue — The <style> block is not scoped (no scoped attribute). CSS classes like .fence-drag-handle, .fence-add-handle, .fence-breach-return, .fence-center-handle are global. While they have relatively specific names, this diverges from the scoped style convention used in most other components. The GeoFenceEditor.vue and GeoFenceParametersPanel.vue correctly use <style scoped>.

6.3 (nit) vehicle.ts — The requestParameter method pads param_id to 16 chars with \0 using a while loop. This is fine functionally, but could be more concise:

const param_id = [...paramName.padEnd(16, '\0')]

6.4 (nit) geoFence.tsclonePlan uses JSON.parse(JSON.stringify(plan)) which is a common deep-clone pattern but loses undefined values (they become missing keys). For breachReturn which can be undefined, this works correctly since the interface treats it as optional, but it's worth noting the structuredClone alternative is available in modern browsers.


7. Tests

No new tests are added for the geofence functionality. Given the complexity of the feature (MAVLink encoding/decoding in geofence-conversion.ts, breach detection logic, plan import/export with type guards), unit tests would be valuable — especially for:

  • convertGeoFencePlanToMavlink / convertMavlinkToGeoFencePlan round-trip correctness
  • detectMissionBreaches with various inclusion/exclusion combinations
  • instanceOfCockpitFencePlanFile / instanceOfMavlinkPlanFile type guards with edge cases

This is noted as a suggestion rather than a blocker, since the existing codebase test coverage appears minimal and this PR is consistent with current practice.


8. Documentation

No README updates. Per AGENTS.md, if a feature differs between Standalone (Electron) and Lite (Web), the limitations should be documented in the README table. The geofence feature appears to work identically in both modes (all MAVLink, no Electron-specific APIs), so no README update is needed.

The in-code JSDoc is thorough across all new files. The PR description is detailed and includes a demo video.


9. Nitpicks / Optional

9.1 (nit) geofence-conversion.ts — The string constants POLYGON_INCLUSION, POLYGON_EXCLUSION, etc. duplicate the enum member names. If the MavCmd enum values ever change their string representation, these would silently break. Consider comparing against the enum directly: item.command.type === MavCmd.MAV_CMD_NAV_FENCE_POLYGON_VERTEX_INCLUSION.

9.2 (nit) geoFence.ts — The @ts-ignore comments in setFenceEnabled and setFenceAutoEnable (inherited from the parameter-setting pattern) would benefit from @ts-expect-error instead, so they break if the type issue is ever fixed upstream.

9.3 (nit) Consider extracting the dialog confirmation pattern (used in confirmPx4MultipleInclusionsIfNeeded and confirmMissionFenceBreachIfNeeded) into a reusable composable, as both follow the exact same Promise<void> + let confirmed = false + resolve() shape.

9.4 (nit) GeoFenceEditor.vue line fenceStore.startDrawingPolygon(true) — the true argument always passes inclusion = true. Users might expect the new polygon to inherit the last-used type. Minor UX consideration.

Generated by Claude. This is advisory; a human reviewer must still approve.

@ArturoManzoli

Copy link
Copy Markdown
Contributor Author

This PR was split into thee others to ease the review:

#2807 - Add geofence vehicle sync and plan types
#2808 - Mission Planning: Add geofence planning editor
#2809 - Add live geofence map overlay

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

docs-needed Change needs to be documented

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants