A swarm is not one robot with many propellers. It is several concurrent actors that share a plan and fail independently.

That sentence is why OTP keeps showing up in multi-drone software. In part 1 we built a single honest vehicle: one GenServer, a pure safety pipeline, UDP to Tello, and a simulator that refuses the same climbs as hardware. ex_drone v0.2.0 does not invent a second architecture for groups. It adds a thin coordinator on top of the same vehicles.

This article is about that coordinator—and about what OTP does not buy you in the air.


Recap: one drone, one GenServer

Each vehicle already owns:

If you centralize all of that into one “fleet GenServer,” you fight the problem:

OTP’s better fit is the boring one: keep one Vehicle process per drone, then add a sibling that knows membership and group operations.

Drone.Swarm  -->  Drone.takeoff(:good)
             -->  Drone.takeoff(:bad)

The swarm orchestrates. The vehicles execute and enforce safety.


The coordinator pattern

Drone.Swarm is a GenServer, but a thin one. It does not own sockets. It does not reimplement altitude checks. It holds an ordered member list, default formation options, and fan-out policy.

Under the application root:

Drone.Supervisor.Root (:one_for_one)
├── Drone.Vehicle.Registry
├── Drone.Swarm.Registry
├── Drone.Supervisor              # DynamicSupervisor → Vehicles
└── Drone.Swarm.Supervisor        # DynamicSupervisor → Swarms

Vehicles and swarms are siblings. A swarm crash does not restart airframes; killing a vehicle does not take down the coordinator. Both use :temporary restart—group flight state is not something you casually resurrect from a blank slate.

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

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

That takeoff/1 is not magic synchronization. It is explicit fan-out.


Registry vs membership

v0.1.0 already solved vehicle identity: Drone.takeoff(:good) looks up a Registry entry. Swarms do not invent a second naming system for drones. They need something else:

  1. an ordered membership list (who is in this group, in what order),
  2. optional naming for the swarm process itself (Drone.Swarm.Registry).

That split mirrors distributed systems: Registry is service discovery; swarm state is cluster membership.

There is a third store for a reason. On application start, ex_drone creates a public ETS table, Drone.Swarm.Members. Normal group ops go through the swarm GenServer mailbox. Emergency does not. Drone.Swarm.emergency/1 runs in the caller, reads membership from ETS, and best-effort stops every vehicle—even if the coordinator is busy inside a long run/2. A kill switch that waits behind choreography is not a kill switch.


Fan-out, fail-fast, and partial results

Group operations return a map of per-member outcomes:

{:ok, %{good: :ok, bad: :ok}}
# or
{:error, :partial, %{good: :ok, bad: {:error, reason}}}

Default policy is :fail_fast: sequential calls (deterministic in tests), halt on first error, do not silently undo members that already succeeded. If one of three takeoffs works, two stay on the ground and one is flying until you land or emergency. That is uncomfortable—and correct. Robotics groups fail partially; pretending otherwise teaches the wrong recovery story.

Lesson for the classroom: group success is an aggregation policy, not a boolean. Partial failure is the normal hard case.


Formations are planners, not pilots

Classic shapes—:front, :column, :vee, :diamond, :echelon, :circle—are pure functions in Drone.Formation:

positions + heading + spacing
  → {:ok, %{drone => Mission}}
  | {:error, reason}

They emit missions the existing DSL already understands. They do not run control loops. They do not hold slots against wind. The default reference is the centroid (or an explicit origin)—not a living leader process. An optional leader: is a plan-time pose snapshot, not mid-flight follow-the-leader.

At plan time, min_separation_cm (default 80) applies Reynolds’ Separate rule to the target slots. Paths between slots are not deconflicted. Live Align / Cohere, closed-loop hold, morphing, and leader election are intentional non-goals for v0.2.0—see the deferred catalogue.

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

run/2 also accepts a map of per-drone missions or a function of the member list. Formations are one convenient producer of those maps.


Shared coordination, local enforcement

Centralizing safety only in the swarm would be a mistake. Members can differ: battery, indoor vs outdoor policy, geofence, prop guards. v0.2.0 keeps Drone.Safety on every Vehicle and adds only plan-time separation in Formation.

Educational punchline: shared coordination, local enforcement.

The coordinator may ask everyone to climb. The vehicle with max_altitude_cm: 50 still says no.


Good Advisor / Bad Advisor

The runnable teaching demo is examples/good_bad_advisor.exs (mix run examples/good_bad_advisor.exs). Two sims share a swarm. :bad carries a tight altitude cap.

good =
  Drone.Mission.new()
  |> Drone.Mission.move(:forward, 40)
  |> Drone.Mission.hover(seconds: 1)

bad =
  Drone.Mission.new()
  |> Drone.Mission.move(:up, 200)

{:error, :partial, results} =
  Drone.Swarm.run(swarm, %{good: good, bad: bad})

# results.good == :ok
# results.bad == {:error, {:safety, :max_altitude}}

Observers should notice three things:

  1. Process isolation — bad’s reject does not crash good’s GenServer.
  2. Partial results — the swarm return value names winners and losers.
  3. Simulator-first — you can rehearse multi-drone failure before any Wi-Fi AP is involved.

Open Observer. You should see two Vehicles and one Swarm. Ask the hard question: if takeoff succeeds for one of three, who lands the orphan? (You do. The library will not invent a silent undo.)


Hardware honesty: software swarm ≠ physics swarm

Stock Tello networking and the lack of a shared global pose make absolute formations unreliable outdoors or across multiple APs. Tello EDU station mode helps connectivity, not localization. You can fan out "takeoff" over UDP. You cannot honestly claim centimeter slot-holding without sensing the library does not provide.

So the boundary is sharp:

OTP models the software swarm; physics and sensing still constrain the hardware swarm.

OTP solves process identity, supervision, messaging, and failure boundaries. It does not solve mid-air physics. Claiming otherwise is how demo videos become incident reports.


Where this wants to live: embedded Elixir and Nerves

Today, the natural home for ex_drone is a ground-station BEAM: a laptop or GCS process tree talking UDP to sims or Tellos. That is what v0.2.0 ships and tests.

The same OTP shape wants a second home on the edge. Nerves is the Elixir project’s toolkit for building small embedded Linux images that boot the Erlang VM early and let an OTP application take over—not a general-purpose distro bolted onto a Pi after the fact (getting started, github.com/nerves-project/nerves). A Nerves application is an OTP supervision tree. That is the entire point.

Three architectures, only one of which is ex_drone’s present tense:

ArchitectureRole of the BEAMStatus
A. Ground-station BEAMSwarm + Vehicles on a laptop/GCS; radios to vehiclesShipped (sim + Tello)
B. Nerves companion computerBEAM on an SBC beside a real autopilot; policy, missions, links, videoRoadmap (ex_drone v1.0 checklist: Nerves + Pi + Tello guide)
C. Autopilot replacementElixir in the inner attitude loopOut of scope — wrong layer

Option B is the industrially honest pattern. Flight controllers such as PX4 or ArduPilot own hard real-time estimation and motor mixing. A companion SBC speaks MAVLink over UART or UDP for missions, telemetry, and higher-level policy. The BEAM is excellent at that companion layer: supervised links, fan-out, safety allowlists, OTA-friendly releases. It is a poor substitute for a purpose-built autopilot firmware loop.

The ecosystem is already exploring the edges of this. Damir Batinović’s NervesConf / Goatmire talk Fly me a camera (summary: Elixir Merge) combines Nerves packaging, Membrane video pipelines, and drone control on the BEAM—control and streaming in one OTP application, on embedded hardware. Projects such as colibri-cam’s Nerves ground-station images show the same gravitational pull: Elixir at the ground station or companion, not as a drop-in PX4.

NervesHub adds a different “fleet” metaphor—firmware updates and device health across many embedded nodes. That is orthogonal to geometric formations, and useful to keep straight in class: device fleets ≠ airframe formations.

ex_drone’s README lists a Nerves integration guide under v1.0.0. Until that lands, treat companion hosting as a destination for this process model, not a feature checkbox in 0.2.0.

Laptop / Nerves GCS          Companion SBC (Nerves)         Flight controller
─────────────────────        ──────────────────────         ─────────────────
Drone.Swarm                  Vehicle adapters / links       PX4 / ArduPilot
Drone.Vehicle × N            safety policy, missions        attitude / motors
     │                              │                              │
     └──────── Wi‑Fi / radio ───────┴──────── MAVLink / UART ──────┘

Same supervision ideas at every tier. Different real-time budgets.


Looking ahead (intentionally deferred)

Do not read the following as “coming next week.” They are catalogued non-goals for v0.2.0 (full list):

Keeping the surface small is what makes the teaching story true: Observer can show the processes; the sim can show the partial failure; the safety reject is local.


Try it

{:ex_drone, "~> 0.2.0"}
mix run examples/good_bad_advisor.exs

Change the bad advisor’s max_altitude_cm. Inject failure_rate on a sim member. Watch fail-fast leave a successful peer flying until you land it. Then—and only then—think about radios.

OTP is a natural model for drone swarms because swarms were always concurrent systems with partial failure. The BEAM did not invent that. It just refuses to let you paper over it.


References and further reading

Prior article and ex_drone

OTP

Swarming and formation control

Embedded Elixir, Nerves, and companions

Tello caveats