defmodule Drone.Mission do
@moduledoc """
Mission DSL for scripting drone command sequences.
A mission is an ordered list of commands that can be validated and
replayed against a drone or simulator.
## Example
mission =
Drone.Mission.new()
|> Drone.Mission.sdk_mode()
|> Drone.Mission.takeoff()
|> Drone.Mission.hover(seconds: 3)
|> Drone.Mission.move(:up, 40)
|> Drone.Mission.move(:forward, 100)
|> Drone.Mission.rotate(:cw, 90)
|> Drone.Mission.land()
Drone.Mission.run(mission, drone)
"""
alias Drone.{Command, Vehicle}
@type t :: %__MODULE__{
commands: [Command.t()],
name: String.t() | nil
}
defstruct commands: [], name: nil
@doc """
Creates a new empty mission.
"""
@spec new(keyword()) :: t()
def new(opts \\ []) do
%__MODULE__{name: Keyword.get(opts, :name)}
end
@doc """
Adds an SDK mode activation command to the mission.
"""
@spec sdk_mode(t()) :: t()
def sdk_mode(%__MODULE__{commands: commands} = mission) do
%{mission | commands: [Command.sdk_mode() | commands]}
end
@doc """
Adds a takeoff command to the mission.
"""
@spec takeoff(t()) :: t()
def takeoff(%__MODULE__{commands: commands} = mission) do
%{mission | commands: [Command.takeoff() | commands]}
end
@doc """
Adds a land command to the mission.
"""
@spec land(t()) :: t()
def land(%__MODULE__{commands: commands} = mission) do
%{mission | commands: [Command.land() | commands]}
end
@doc """
Adds an emergency stop command to the mission.
"""
@spec emergency(t()) :: t()
def emergency(%__MODULE__{commands: commands} = mission) do
%{mission | commands: [Command.emergency() | commands]}
end
@doc """
Adds a movement command to the mission.
"""
@spec move(t(), Command.direction(), pos_integer()) :: t()
def move(%__MODULE__{commands: commands} = mission, direction, distance) do
%{mission | commands: [Command.move(direction, distance) | commands]}
end
@doc """
Adds a rotation command to the mission.
"""
@spec rotate(t(), Command.rotation(), pos_integer()) :: t()
def rotate(%__MODULE__{commands: commands} = mission, direction, degrees) do
%{mission | commands: [Command.rotate(direction, degrees) | commands]}
end
@doc """
Adds a flip command to the mission.
"""
@spec flip(t(), Command.flip_direction()) :: t()
def flip(%__MODULE__{commands: commands} = mission, direction) do
%{mission | commands: [Command.flip(direction) | commands]}
end
@doc """
Adds a hover command to the mission.
"""
@spec hover(t(), keyword()) :: t()
def hover(%__MODULE__{commands: commands} = mission, opts \\ []) do
seconds = Keyword.get(opts, :seconds, 1)
%{mission | commands: [Command.hover(seconds) | commands]}
end
@doc """
Adds a speed setting command to the mission.
"""
@spec speed(t(), pos_integer()) :: t()
def speed(%__MODULE__{commands: commands} = mission, speed) do
%{mission | commands: [Command.speed(speed) | commands]}
end
@doc """
Adds a stop command to the mission.
"""
@spec stop(t()) :: t()
def stop(%__MODULE__{commands: commands} = mission) do
%{mission | commands: [Command.stop() | commands]}
end
@doc """
Adds a query command to the mission.
"""
@spec query(t(), Command.query_type()) :: t()
def query(%__MODULE__{commands: commands} = mission, type) do
%{mission | commands: [Command.query(type) | commands]}
end
@doc """
Returns the list of commands in the mission in execution order.
"""
@spec commands(t()) :: [Command.t()]
def commands(%__MODULE__{commands: commands}), do: Enum.reverse(commands)
@doc """
Returns the number of commands in the mission.
"""
@spec length(t()) :: non_neg_integer()
def length(%__MODULE__{commands: commands}), do: Kernel.length(commands)
@doc """
Runs a mission against a drone process.
Each command is sent sequentially. If any command fails, the mission
stops and returns the error.
Returns `{:ok, results}` with a list of results for each command,
or `{:error, command, reason}` with the failing command and error.
"""
@spec run(t(), atom()) :: {:ok, [term()]} | {:error, Command.t(), term()}
def run(%__MODULE__{commands: commands}, drone_name) do
pid = Vehicle.whereis(drone_name)
if pid do
run_commands(Enum.reverse(commands), pid, [])
else
{:error, Command.sdk_mode(), {:no_process, drone_name}}
end
end
defp run_commands([], _pid, results), do: {:ok, Enum.reverse(results)}
defp run_commands([cmd | rest], pid, results) do
case GenServer.call(pid, {:command, cmd}) do
{:ok, reply} ->
run_commands(rest, pid, [reply | results])
{:error, :safety, reason} ->
{:error, cmd, {:safety, reason}}
{:error, reason} ->
{:error, cmd, reason}
end
end
end