Skip to main content

lib/drone/safety/policy.ex

defmodule Drone.Safety.Policy do
  @moduledoc """
  Safety policy struct and defaults.

  A policy defines the safety rules applied to every command before it
  reaches the drone adapter. Policies are configured at connection time
  and cannot be changed while a drone is connected.

  ## Presets

    - `Drone.Safety.Policy.default/0` -- safe defaults for outdoor flight
    - `Drone.Safety.Policy.indoor/0` -- tighter limits for indoor flight
    - `Drone.Safety.Policy.unrestricted/0` -- no safety limits (use with caution)

  ## Example

  Safety options can be passed to `Drone.connect/2` either as a keyword list
  (built into a policy via `new/1`) or as an already-constructed `%Policy{}`:

      {:ok, drone} = Drone.connect(:sim, name: :test, safety: [max_altitude_cm: 200, indoor: true])

      policy = Drone.Safety.Policy.new(max_altitude_cm: 200, indoor: true)
      {:ok, drone} = Drone.connect(:sim, name: :test, safety: policy)
  """

  @type t :: %__MODULE__{
          max_altitude_cm: pos_integer() | nil,
          max_distance_cm: pos_integer() | nil,
          min_battery_percent: non_neg_integer(),
          battery_warning_percent: non_neg_integer(),
          allowlist: [atom()] | nil,
          dry_run: boolean(),
          indoor: boolean(),
          prop_guards: boolean(),
          geofence: Drone.Safety.Geofence.t() | nil
        }

  defstruct [
    :max_altitude_cm,
    :max_distance_cm,
    :geofence,
    min_battery_percent: 15,
    battery_warning_percent: 20,
    allowlist: nil,
    dry_run: false,
    indoor: false,
    prop_guards: false
  ]

  @doc """
  Creates a new policy with the given options.

  Options:

    - `:max_altitude_cm` -- maximum allowed altitude in cm (default: 300)
    - `:max_distance_cm` -- maximum distance from launch point in cm (default: 1000)
    - `:min_battery_percent` -- minimum battery for takeoff (default: 15)
    - `:battery_warning_percent` -- battery level for warnings (default: 20)
    - `:allowlist` -- list of allowed command types, or nil for all (default: nil)
    - `:dry_run` -- if true, commands pass safety but are not sent (default: false)
    - `:indoor` -- if true, applies indoor preset limits (default: false)
    - `:unrestricted` -- if true, applies the unrestricted preset (no limits)
    - `:prop_guards` -- whether prop guards are installed (default: false)
    - `:geofence` -- a geofence to restrict flight area (default: nil)
  """
  @spec new(keyword()) :: t()
  def new(opts \\ []) do
    base = base_preset(opts)

    %__MODULE__{
      max_altitude_cm: Keyword.get(opts, :max_altitude_cm, base.max_altitude_cm),
      max_distance_cm: Keyword.get(opts, :max_distance_cm, base.max_distance_cm),
      min_battery_percent: Keyword.get(opts, :min_battery_percent, base.min_battery_percent),
      battery_warning_percent:
        Keyword.get(opts, :battery_warning_percent, base.battery_warning_percent),
      allowlist: Keyword.get(opts, :allowlist, base.allowlist),
      dry_run: Keyword.get(opts, :dry_run, base.dry_run),
      indoor: Keyword.get(opts, :indoor, base.indoor),
      prop_guards: Keyword.get(opts, :prop_guards, base.prop_guards),
      geofence: Keyword.get(opts, :geofence, base.geofence)
    }
  end

  defp base_preset(opts) do
    cond do
      Keyword.get(opts, :unrestricted, false) -> unrestricted()
      Keyword.get(opts, :indoor, false) -> indoor()
      true -> default()
    end
  end

  @doc """
  Default safety policy for outdoor flight.

    - Max altitude: 300 cm (3 meters)
    - Max distance: 1000 cm (10 meters)
    - Min battery: 15%
    - Battery warning: 20%
  """
  @spec default() :: t()
  def default do
    %__MODULE__{
      max_altitude_cm: 300,
      max_distance_cm: 1000,
      min_battery_percent: 15,
      battery_warning_percent: 20,
      allowlist: nil,
      dry_run: false,
      indoor: false,
      prop_guards: false,
      geofence: nil
    }
  end

  @doc """
  Indoor safety policy with tighter limits.

    - Max altitude: 200 cm (2 meters)
    - Max distance: 500 cm (5 meters)
    - Min battery: 20%
    - Battery warning: 25%
    - Prop guards assumed: true
  """
  @spec indoor() :: t()
  def indoor do
    %__MODULE__{
      max_altitude_cm: 200,
      max_distance_cm: 500,
      min_battery_percent: 20,
      battery_warning_percent: 25,
      allowlist: nil,
      dry_run: false,
      indoor: true,
      prop_guards: true,
      geofence: nil
    }
  end

  @doc """
  Unrestricted safety policy with no limits.

  Use with extreme caution. This disables all altitude, distance,
  and battery checks. Emergency commands still work.
  """
  @spec unrestricted() :: t()
  def unrestricted do
    %__MODULE__{
      max_altitude_cm: nil,
      max_distance_cm: nil,
      min_battery_percent: 0,
      battery_warning_percent: 0,
      allowlist: nil,
      dry_run: false,
      indoor: false,
      prop_guards: true,
      geofence: nil
    }
  end
end