Drone.Formation (ex_drone v0.2.0)

View Source

Pure geometric formation planners for swarm coordination.

Formations compute target slots in a shared world frame and return per-drone Drone.Mission scripts that fly each member to its slot. They do not run control loops, velocity sync, or flocking behaviours (Separate / Align / Cohere live). Plan-time Separate is enforced via min_separation_cm.

Supported formations

  • :front — side-by-side, perpendicular to heading (sweeps / advance)
  • :column — nose-to-tail along heading (narrow corridors)
  • :vee — V opening opposite travel (geometry only; no aero model)
  • :diamond — four-sided closed layout (requires 4 drones)
  • :echelon — diagonal stepped rank (side: :left | :right)

  • :circle — evenly spaced on a ring (radius_cm)
  • :shoulder_pair — alias for :front with two members
  • :grid — optional classroom row/column lattice (columns)

Reference frame

Default origin is the centroid of current positions, or a configured {:xy, x, y}. Optional :leader uses that member's pose at plan time only — not a live runtime dependency. Yaw 0 means +Y (same convention as the shared geometry helpers).

Movement strategy

For each drone: rotate to face the target bearing → move forward (split into ≤500 cm segments) → restore original yaw when needed. Horizontal moves shorter than 20 cm are skipped (SDK minimum).

Example

{:ok, missions} =
  Drone.Formation.plan(:front, %{
    drones: [:a, :b],
    positions: %{
      a: %{x: 0, y: 0, z: 30, yaw: 0},
      b: %{x: 0, y: 0, z: 30, yaw: 0}
    },
    heading_deg: 0,
    spacing_cm: 100,
    min_separation_cm: 80,
    origin: {:xy, 0, 0}
  })

Drone.Mission.run(missions.a, :a)

Summary

Types

Built-in formation identifier.

Error atoms returned by plan/2.

Options for plan/2.

World-frame pose for one drone used as planning input.

Functions

Plans missions that move each drone into the given formation.

Types

formation()

@type formation() ::
  :front
  | :column
  | :vee
  | :diamond
  | :echelon
  | :circle
  | :shoulder_pair
  | :grid

Built-in formation identifier.

ValueMin dronesLayout
:front2Side-by-side ⊥ heading
:column2Along heading
:vee3V behind tip
:diamond4N/E/S/W of origin
:echelon2Diagonal stepped rank
:circle3Ring around origin
:shoulder_pair2Alias of :front
:grid2Row/column lattice

Examples

:front
:vee
:echelon

plan_error()

@type plan_error() ::
  :separation_violation
  | :unsupported_formation
  | :too_few_drones
  | :too_many_drones
  | :duplicate_drones
  | :invalid_option
  | :leader_unavailable
  | :missing_positions
  | :unassigned_slot

Error atoms returned by plan/2.

  • :separation_violation — two planned slots closer than min_separation_cm
  • :unsupported_formation — unknown formation atom
  • :too_few_drones — membership below the formation minimum
  • :too_many_drones — membership above a formation maximum (e.g. :diamond)
  • :duplicate_drones — repeated names in :drones
  • :invalid_option — bad spacing, columns, side, heading, etc.
  • :leader_unavailable:leader missing from :positions
  • :missing_positions — a member lacks :x/:y
  • :unassigned_slot — planner/assembly mismatch (should not occur)

Example

:separation_violation

plan_opts()

@type plan_opts() :: %{
  optional(:drones) => [atom()],
  optional(:positions) => %{optional(atom()) => position()},
  optional(:heading_deg) => integer(),
  optional(:spacing_cm) => pos_integer(),
  optional(:min_separation_cm) => pos_integer(),
  optional(:origin) => :centroid | {:xy, number(), number()},
  optional(:leader) => atom(),
  optional(:side) => :left | :right,
  optional(:radius_cm) => pos_integer(),
  optional(:columns) => pos_integer()
}

Options for plan/2.

Fields

KeyTypeDefaultMeaning
:drones[atom()]requiredOrdered member names
:positions%{atom() => position()}requiredCurrent poses
:heading_deginteger()0Direction of motion
:spacing_cmpos_integer()100Neighbor spacing
:min_separation_cmpos_integer()80Reject closer planned slots
:origin:centroid | {:xy, number(), number()}:centroidFormation origin
:leaderatom()nonePlan-time origin = that member's pose
:side:left | :right:rightEchelon side
:radius_cmpos_integer()spacing_cmCircle radius
:columnspos_integer()ceil(sqrt(n))Grid column count

Example

%{
  drones: [:a, :b, :c],
  positions: %{
    a: %{x: 0, y: 0, yaw: 0},
    b: %{x: 0, y: 0, yaw: 0},
    c: %{x: 0, y: 0, yaw: 0}
  },
  heading_deg: 0,
  spacing_cm: 100,
  min_separation_cm: 80,
  origin: :centroid,
  side: :left,
  radius_cm: 150,
  columns: 2
}

position()

@type position() :: %{
  :x => number(),
  :y => number(),
  optional(:z) => number(),
  optional(:yaw) => number()
}

World-frame pose for one drone used as planning input.

Fields

FieldTypeRequiredMeaning
:xnumber()yesEast/west cm in shared world frame
:ynumber()yesNorth/south cm (yaw 0 faces +Y)
:znumber()noAltitude cm (unused by horizontal planners)
:yawnumber()noHeading degrees 0..359 (default 0)

Example

%{x: -50, y: 0, z: 30, yaw: 90}

Functions

plan(formation, opts)

@spec plan(formation(), plan_opts() | keyword()) ::
  {:ok, %{required(atom()) => Drone.Mission.t()}} | {:error, plan_error()}

Plans missions that move each drone into the given formation.

Pure function: no processes, no I/O. Returns missions; callers execute them (e.g. via Drone.Swarm.run/2 or Drone.Mission.run/2).

Positions accept numeric :x/:y/:yaw; values are rounded to integers before path generation. Moves shorter than 20 cm are skipped (SDK minimum); longer legs are split into segments of at most 500 cm with any remainder redistributed so the planned slot is reached exactly.

Parameters

  • formation (formation/0) — which layout to compute
  • opts (plan_opts/0 or keyword()) — drones, positions, spacing, etc.

Returns

  • {:ok, %{atom() => Drone.Mission.t()}} — one mission per drone
  • {:error, plan_error()} — validation / geometry failure

Examples

{:ok, missions} =
  Drone.Formation.plan(:front,
    drones: [:left, :right],
    positions: %{
      left: %{x: 0, y: 0, yaw: 0},
      right: %{x: 0, y: 0, yaw: 0}
    },
    spacing_cm: 100,
    origin: {:xy, 0, 0}
  )

{:error, :separation_violation} =
  Drone.Formation.plan(:front, %{
    drones: [:a, :b],
    positions: %{a: %{x: 0, y: 0}, b: %{x: 0, y: 0}},
    spacing_cm: 40,
    min_separation_cm: 80,
    origin: {:xy, 0, 0}
  })