Drone.Swarm (ex_drone v0.3.0)

View Source

Coordinator for a named set of drones.

A swarm owns membership and group operations. Each member remains a normal Drone.Vehicle under Drone.Supervisor. Per-drone Drone.Safety still gates every command; the swarm adds plan-time formation separation and fail-fast fan-out for coordinated commands.

Formations are one-shot geometric planners (Drone.Formation), not continuous flocking controllers.

Quick start

{:ok, swarm} =
  Drone.Swarm.start(
    name: :advisors,
    members: [
      {:good, adapter: :sim, initial_x: 0, initial_y: 0},
      {:bad, adapter: :sim, initial_x: 0, initial_y: 0}
    ],
    spacing_cm: 100,
    min_separation_cm: 80
  )

{:ok, _} = Drone.Swarm.connect_sdk(swarm)
{:ok, _} = Drone.Swarm.takeoff(swarm)
{:ok, _} = Drone.Swarm.run(swarm, :front)
{:ok, _} = Drone.Swarm.land(swarm)
:ok = Drone.Swarm.stop(swarm)

Member-list sugar (anonymous swarm, returns a pid):

{:ok, swarm} =
  Drone.Swarm.start([
    {:left, adapter: :sim, initial_x: -50},
    {:right, adapter: :sim, initial_x: 50}
  ])

Result shapes

Coordinated ops return a map of per-member outcomes:

{:ok, %{good: :ok, bad: :ok}}
{:error, :partial, %{good: :ok, bad: {:error, {:safety, :max_altitude}}}}

Default policy is fail-fast: stop issuing further member commands after the first error. Already-completed members are not undone; call land/1 or emergency/1 explicitly.

emergency/1 fans out from the caller process using a membership table, so it is not blocked by an in-flight run/2 or other coordinated call.

See also

  • Drone.Formation — formation planners
  • docs/swarm.md — user guide
  • docs/design/v0_2_0_deferred.md — deferred flocking / closed-loop work

Summary

Types

Outcome for a single swarm member after a coordinated operation.

Map of member name → per-member result for a coordinated operation.

Options accepted by start/1 after normalization.

Handle used to address a running swarm.

Functions

Returns the child specification used by Drone.Swarm.Supervisor.

Sends SDK-mode activation to every member (fail-fast).

Emergency-stops every member (best-effort; does not fail-fast).

Coordinated land for every member (fail-fast).

Returns the ordered list of member drone names.

Runs a formation, a per-drone mission map, or a custom function.

Starts a supervised swarm and connects all members.

Stops the swarm process.

Coordinated takeoff for every member (fail-fast).

Returns a map of telemetry snapshots for every member.

Looks up a named swarm process.

Types

member_result()

@type member_result() :: :ok | {:ok, term()} | {:error, term()}

Outcome for a single swarm member after a coordinated operation.

  • :ok — command succeeded with no payload
  • {:ok, term()} — succeeded with a value (e.g. mission reply list)
  • {:error, term()} — failed; reason may be {:safety, atom()}, :simulated_failure, :not_connected, etc.

Examples

:ok
{:ok, [:ok, :ok]}
{:error, {:safety, :max_altitude}}
{:error, :simulated_failure}

results()

@type results() :: %{optional(atom()) => member_result()}

Map of member name → per-member result for a coordinated operation.

Keys are the drone names from membership (atoms such as :good, :left).

Example

%{good: :ok, bad: {:error, {:safety, :max_altitude}}}

start_opts()

@type start_opts() :: [
  name: atom(),
  members: [{atom(), keyword()}],
  spacing_cm: pos_integer(),
  min_separation_cm: pos_integer(),
  heading_deg: integer(),
  timeout: timeout()
]

Options accepted by start/1 after normalization.

Fields / keys

KeyTypeDefaultMeaning
:nameatom()noneRegister swarm in Drone.Swarm.Registry
:members[{atom(), keyword()}]requiredOrdered member list
:spacing_cmpos_integer()100Neighbor spacing for formations
:min_separation_cmpos_integer()80Plan-time Separate check
:heading_deginteger()0Formation travel heading (yaw 0 = +Y)
:timeouttimeout()60_000Default GenServer.call timeout for ops

Each member keyword list is passed to Drone.connect/2 (plus :name). Common member opts: :adapter (:sim | :tello | module), :initial_x, :initial_y, :initial_z, :initial_yaw, :safety, simulator failure injection opts.

Example

[
  name: :patrol,
  members: [
    {:alpha, adapter: :sim, initial_x: -50, safety: [indoor: true]},
    {:bravo, adapter: :sim, initial_x: 50}
  ],
  spacing_cm: 120,
  min_separation_cm: 100,
  heading_deg: 0
]

swarm()

@type swarm() :: atom() | pid()

Handle used to address a running swarm.

  • atom() — registered name passed as :name to start/1 (looked up via Drone.Swarm.Registry), e.g. :advisors
  • pid() — process id of an anonymous swarm

Examples

:advisors
#PID<0.123.0>

Functions

child_spec(init_arg)

@spec child_spec(start_opts()) :: Supervisor.child_spec()

Returns the child specification used by Drone.Swarm.Supervisor.

Parameters

Returns

A supervisor child spec map with :temporary restart and a shutdown allowance for member disconnects.

Example

Drone.Swarm.child_spec(name: :demo, members: [{:a, adapter: :sim}, {:b, adapter: :sim}])

connect_sdk(swarm, opts \\ [])

@spec connect_sdk(
  swarm(),
  keyword()
) :: {:ok, results()} | {:error, :partial, results()} | {:error, :not_found}

Sends SDK-mode activation to every member (fail-fast).

Parameters

  • swarm (swarm/0) — swarm name or pid
  • opts (keyword()) — :timeout overrides the swarm default

Returns

  • {:ok, results()} — all members entered SDK mode
  • {:error, :partial, results()} — stopped after first failure
  • {:error, :not_found}

Example

{:ok, %{good: :ok, bad: :ok}} = Drone.Swarm.connect_sdk(:advisors)

emergency(swarm)

@spec emergency(swarm()) :: {:ok, results()} | {:error, :not_found}

Emergency-stops every member (best-effort; does not fail-fast).

Fans out from the caller process using the swarm membership table, so this is not queued behind an in-flight run/2, takeoff/1, or other coordinator call. Continues through all members even if one fails. Bypasses normal safety on each vehicle.

Parameters

  • swarm (swarm/0) — swarm name or pid

Returns

  • {:ok, results()} — always returns the full per-member map when the swarm exists
  • {:error, :not_found}

Example

{:ok, %{good: :ok, bad: :ok}} = Drone.Swarm.emergency(:advisors)

land(swarm, opts \\ [])

@spec land(
  swarm(),
  keyword()
) :: {:ok, results()} | {:error, :partial, results()} | {:error, :not_found}

Coordinated land for every member (fail-fast).

Parameters

  • swarm (swarm/0) — swarm name or pid
  • opts (keyword()) — :timeout overrides the swarm default

Returns

  • {:ok, results()}
  • {:error, :partial, results()}
  • {:error, :not_found}

Example

{:ok, _} = Drone.Swarm.land(:advisors)

members(swarm)

@spec members(swarm()) :: {:ok, [atom()]} | {:error, :not_found}

Returns the ordered list of member drone names.

Parameters

  • swarm (swarm/0) — swarm name or pid

Returns

  • {:ok, [atom()]} — membership order, e.g. {:ok, [:good, :bad]}
  • {:error, :not_found} — unknown swarm

Example

{:ok, [:good, :bad]} = Drone.Swarm.members(:advisors)

run(swarm, target, opts \\ [])

@spec run(
  swarm(),
  Drone.Formation.formation()
  | %{required(atom()) => Drone.Mission.t()}
  | (map() -> term()),
  keyword()
) :: {:ok, results()} | {:error, term()} | {:error, :partial, results()}

Runs a formation, a per-drone mission map, or a custom function.

When target is a formation atom, opts may include formation planner options (:heading_deg, :spacing_cm, :min_separation_cm, :leader, :origin, :side, :radius_cm, :columns) which override swarm defaults. Use :timeout to override the call timeout.

Parameters

  • swarm (swarm/0) — swarm name or pid
  • target — one of:
    • Drone.Formation.formation() — e.g. :front, :vee, :circle
    • %{atom() => Drone.Mission.t()} — per-member missions (membership order; unknown keys are rejected before any flight)
    • (map() -> term()) — function receiving %{name => name} member map; must return :ok, {:ok, term()}, or {:error, term()}
  • opts (keyword()) — formation and/or :timeout options

Returns

  • {:ok, results()} — all members succeeded (or custom function returned ok)
  • {:error, :partial, results()} — fail-fast stop during mission execution
  • {:error, reason} — plan-time error such as :separation_violation, :too_few_drones, :unsupported_formation, :unsupported_run_target, {:unknown_members, [atom()]}, {:invalid_run_result, term()}
  • {:error, :not_found} — unknown swarm

Examples

{:ok, _} = Drone.Swarm.run(swarm, :front)
{:ok, _} = Drone.Swarm.run(swarm, :echelon, side: :left, heading_deg: 90)

good = Drone.Mission.new() |> Drone.Mission.move(:forward, 40)
bad = Drone.Mission.new() |> Drone.Mission.move(:up, 200)
{:error, :partial, results} = Drone.Swarm.run(swarm, %{good: good, bad: bad})

{:ok, _} =
  Drone.Swarm.run(swarm, fn members ->
    Enum.each(members, fn {_k, name} -> Drone.hover(name, seconds: 1) end)
    :ok
  end)

start(arg)

@spec start(start_opts() | [{atom(), keyword()}] | [atom()]) ::
  {:ok, swarm()} | {:error, term()}

Starts a supervised swarm and connects all members.

Accepts either:

  1. A keyword list with :members and optional swarm options
  2. A bare member list [{name, opts}, ...] or [name, ...] (defaults to :sim)

Members are started via Drone.connect/2 under Drone.Supervisor. The swarm process itself is started under Drone.Swarm.Supervisor.

Like Drone.connect/2, this returns a swarm handle (:name or pid) and does not link the caller. Use child_spec/1 under your own supervisor when you need OTP linking/restart semantics.

Parameters

  • arg (t:start_opts/0 | [{atom(), keyword()}] | [atom()]) — start options or member list

Returns

  • {:ok, swarm()} — swarm name when :name was given, otherwise the pid
  • {:error, :name_already_taken} — swarm name already registered
  • {:error, term()} — member connect failure or supervisor error

Examples

{:ok, :advisors} =
  Drone.Swarm.start(
    name: :advisors,
    members: [
      {:good, adapter: :sim, initial_x: 0},
      {:bad, adapter: :sim, initial_x: 0, safety: [max_altitude_cm: 50]}
    ]
  )

{:ok, pid} =
  Drone.Swarm.start([
    {:left, adapter: :sim, initial_x: -50},
    {:right, adapter: :sim, initial_x: 50}
  ])

stop(swarm, opts \\ [])

@spec stop(
  swarm(),
  keyword()
) :: :ok | {:error, term()}

Stops the swarm process.

By default disconnects all member vehicles. Pass disconnect: false to leave vehicles running after the coordinator exits.

Parameters

  • swarm (swarm/0) — swarm name or pid
  • opts (keyword()) — options:
    • :disconnect (boolean(), default true) — disconnect members
    • :timeout (timeout()) — call timeout

Returns

  • :ok
  • {:error, :not_found}

Examples

:ok = Drone.Swarm.stop(:advisors)
:ok = Drone.Swarm.stop(swarm, disconnect: false)

takeoff(swarm, opts \\ [])

@spec takeoff(
  swarm(),
  keyword()
) :: {:ok, results()} | {:error, :partial, results()} | {:error, :not_found}

Coordinated takeoff for every member (fail-fast).

Members must already be in SDK mode (connect_sdk/1).

Parameters

  • swarm (swarm/0) — swarm name or pid
  • opts (keyword()) — :timeout overrides the swarm default

Returns

  • {:ok, results()}
  • {:error, :partial, results()}
  • {:error, :not_found}

Example

{:ok, _} = Drone.Swarm.takeoff(:advisors)

telemetry(swarm, opts \\ [])

@spec telemetry(
  swarm(),
  keyword()
) :: {:ok, %{required(atom()) => map()}} | {:error, term()}

Returns a map of telemetry snapshots for every member.

Parameters

  • swarm (swarm/0) — swarm name or pid
  • opts (keyword()) — :timeout overrides the swarm default

Returns

  • {:ok, %{atom() => map()}} — keys are member names; values include at least :x, :y, :z, :yaw, :battery, :flying, :mode
  • {:error, {member :: atom(), reason :: term()}} — first telemetry failure
  • {:error, :not_found} — unknown swarm

Example

{:ok, tel} = Drone.Swarm.telemetry(:advisors)
tel.good.flying
#=> true

whereis(name)

@spec whereis(atom()) :: pid() | nil

Looks up a named swarm process.

Parameters

  • name (atom()) — registry key, e.g. :advisors

Returns

  • pid() when registered
  • nil when not found

Example

pid = Drone.Swarm.whereis(:advisors)