Skip to main content

lib/drone/command.ex

defmodule Drone.Command do
  @moduledoc """
  Command struct and helpers for drone operations.

  Every command sent through the ex_drone pipeline is represented as a
  `Drone.Command` struct. This provides a unified representation regardless
  of which adapter handles the command.
  """

  @type direction :: :up | :down | :left | :right | :forward | :back
  @type rotation :: :cw | :ccw
  @type flip_direction :: :left | :right | :forward | :back
  @type query_type :: :battery | :height | :speed | :time | :wifi | :sdk_version | :serial_number
  @type command_type ::
          :sdk_mode
          | :takeoff
          | :land
          | :emergency
          | :move
          | :rotate
          | :flip
          | :hover
          | :speed
          | :stop
          | :query

  @type t :: %__MODULE__{
          type: command_type(),
          args: keyword(),
          raw: String.t() | nil
        }

  @enforce_keys [:type]
  defstruct [:type, :args, :raw]

  @doc """
  Creates a new command struct.
  """
  @spec new(command_type(), keyword()) :: t()
  def new(type, args \\ []) do
    %__MODULE__{type: type, args: args}
  end

  @doc """
  Creates an SDK mode activation command.
  """
  @spec sdk_mode() :: t()
  def sdk_mode, do: new(:sdk_mode)

  @doc """
  Creates a takeoff command.
  """
  @spec takeoff() :: t()
  def takeoff, do: new(:takeoff)

  @doc """
  Creates a land command.
  """
  @spec land() :: t()
  def land, do: new(:land)

  @doc """
  Creates an emergency stop command.
  """
  @spec emergency() :: t()
  def emergency, do: new(:emergency)

  @doc """
  Creates a movement command.

  Direction must be one of: `:up`, `:down`, `:left`, `:right`, `:forward`, `:back`.
  Distance must be between 20 and 500 cm.
  """
  @spec move(direction(), pos_integer()) :: t()
  def move(direction, distance) when direction in [:up, :down, :left, :right, :forward, :back] do
    new(:move, direction: direction, distance: distance)
  end

  @doc """
  Creates a rotation command.

  Direction must be `:cw` (clockwise) or `:ccw` (counter-clockwise).
  Degrees must be between 1 and 3600.
  """
  @spec rotate(rotation(), pos_integer()) :: t()
  def rotate(direction, degrees) when direction in [:cw, :ccw] do
    new(:rotate, direction: direction, degrees: degrees)
  end

  @doc """
  Creates a flip command.

  Direction must be one of: `:left`, `:right`, `:forward`, `:back`.
  """
  @spec flip(flip_direction()) :: t()
  def flip(direction) when direction in [:left, :right, :forward, :back] do
    new(:flip, direction: direction)
  end

  @doc """
  Creates a hover command.

  Seconds must be a positive integer.
  """
  @spec hover(pos_integer()) :: t()
  def hover(seconds) do
    new(:hover, seconds: seconds)
  end

  @doc """
  Creates a speed setting command.

  Speed must be between 10 and 100 cm/s.
  """
  @spec speed(pos_integer()) :: t()
  def speed(speed) do
    new(:speed, speed: speed)
  end

  @doc """
  Creates a stop command (hover in place).
  """
  @spec stop() :: t()
  def stop, do: new(:stop)

  @doc """
  Creates a query command.

  Query type must be one of: `:battery`, `:height`, `:speed`, `:time`,
  `:wifi`, `:sdk_version`, `:serial_number`.
  """
  @spec query(query_type()) :: t()
  def query(type)
      when type in [:battery, :height, :speed, :time, :wifi, :sdk_version, :serial_number] do
    new(:query, type: type)
  end

  @doc """
  Checks if a command is an emergency command.
  """
  @spec emergency?(t()) :: boolean()
  def emergency?(%__MODULE__{type: :emergency}), do: true
  def emergency?(_), do: false

  @doc """
  Checks if a command is a movement command.
  """
  @spec movement?(t()) :: boolean()
  def movement?(%__MODULE__{type: type}) when type in [:move, :rotate, :flip], do: true
  def movement?(_), do: false

  @doc """
  Checks if a command is a query command.
  """
  @spec query?(t()) :: boolean()
  def query?(%__MODULE__{type: :query}), do: true
  def query?(_), do: false

  @doc """
  Checks if a command requires the drone to be flying.
  """
  @spec requires_flying?(t()) :: boolean()
  def requires_flying?(%__MODULE__{type: type})
      when type in [:move, :rotate, :flip, :hover, :stop],
      do: true

  def requires_flying?(%__MODULE__{type: :land}), do: true
  def requires_flying?(_), do: false

  @doc """
  Checks if a command is a safe (retryable) command.
  """
  @spec safe_to_retry?(t()) :: boolean()
  def safe_to_retry?(%__MODULE__{type: :query}), do: true
  def safe_to_retry?(%__MODULE__{type: :sdk_mode}), do: true
  def safe_to_retry?(_), do: false

  @doc """
  Returns all valid command types.
  """
  @spec types() :: [command_type()]
  def types do
    [:sdk_mode, :takeoff, :land, :emergency, :move, :rotate, :flip, :hover, :speed, :stop, :query]
  end
end