lib/gpio.ex

# SPDX-FileCopyrightText: 2018 Frank Hunleth, Mark Sebald, Matt Ludwigs
#
# SPDX-License-Identifier: Apache-2.0

defmodule Circuits.GPIO do
  @moduledoc """
  Control GPIOs from Elixir

  If you're coming from Elixir/ALE, check out our [porting guide](PORTING.md).

  `Circuits.GPIO` works great with LEDs, buttons, many kinds of sensors, and
  simple control of motors. In general, if a device requires high speed
  transactions or has hard real-time constraints in its interactions, this is not
  the right library. For those devices, see if there's a Linux kernel driver.
  """
  alias Circuits.GPIO.Handle

  @typedoc """
  Backends specify an implementation of a Circuits.GPIO.Backend behaviour

  The second parameter of the Backend 2-tuple is a list of options. These are
  passed to the behaviour function call implementations.
  """
  @type backend() :: {module(), keyword()}

  @typedoc """
  The names or numbers for one or more GPIO pins

  See your device's documentation for how pins are labelled on your
  device. Currently only numbers are supported by backends, but future backends
  should support other ways.
  """
  @type pin_spec() :: non_neg_integer()

  @typedoc "The GPIO direction (input or output)"
  @type pin_direction() :: :input | :output

  @typedoc "GPIO logic value (low = 0 or high = 1)"
  @type value() :: 0 | 1

  @typedoc "Trigger edge for pin change notifications"
  @type trigger() :: :rising | :falling | :both | :none

  @typedoc "Pull mode for platforms that support controllable pullups and pulldowns"
  @type pull_mode() :: :not_set | :none | :pullup | :pulldown

  @typedoc """
  Options for `open/3`
  """
  @type open_options() :: [initial_value: value() | :not_set, pull_mode: pull_mode()]

  @typedoc """
  Options for `set_interrupt/2`
  """
  @type interrupt_options() :: [suppress_glitches: boolean(), receiver: pid() | atom()]

  # Public API

  @doc """
  Open a GPIO for use.

  `pin` should be a valid GPIO pin number on the system and `pin_direction`
  should be `:input` or `:output`. If opening as an output, then be sure to set
  the `:initial_value` option if you need the set to be glitch free.

  Options:

  * :initial_value - Set to `:not_set`, `0` or `1` if this is an output.
    `:not_set` is the default.
  * :pull_mode - Set to `:not_set`, `:pullup`, `:pulldown`, or `:none` for an
     input pin. `:not_set` is the default.
  """
  @spec open(pin_spec(), pin_direction(), open_options()) ::
          {:ok, Handle.t()} | {:error, atom()}
  def open(pin_number, pin_direction, options \\ []) do
    check_options!(options)

    {backend, backend_defaults} = default_backend()

    all_options =
      backend_defaults
      |> Keyword.merge(options)
      |> Keyword.put_new(:initial_value, :not_set)
      |> Keyword.put_new(:pull_mode, :not_set)

    backend.open(pin_number, pin_direction, all_options)
  end

  defp check_options!([]), do: :ok

  defp check_options!([{:initial_value, value} | rest]) do
    unless value in [:not_set, 0, 1],
      do: raise(ArgumentError, ":initial_value should be :not_set, 0, or 1")

    check_options!(rest)
  end

  defp check_options!([{:pull_mode, value} | rest]) do
    unless value in [:not_set, :pullup, :pulldown, :none],
      do: raise(ArgumentError, ":pull_mode should be :not_set, :pullup, :pulldown, or :none")

    check_options!(rest)
  end

  defp check_options!([_unknown_option | rest]) do
    # Ignore unknown options - the backend might use them
    check_options!(rest)
  end

  @doc """
  Release the resources associated with the GPIO.

  This is optional. The garbage collector will free GPIO resources that aren't in
  use, but this will free them sooner.
  """
  @spec close(Handle.t()) :: :ok
  defdelegate close(handle), to: Handle

  @doc """
  Read the current value on a pin.
  """
  @spec read(Handle.t()) :: value()
  defdelegate read(handle), to: Handle

  @doc """
  Set the value of a pin. The pin should be configured to an output
  for this to work.
  """
  @spec write(Handle.t(), value()) :: :ok
  defdelegate write(handle, value), to: Handle

  @doc """
  Enable or disable pin value change notifications. The notifications
  are sent based on the trigger parameter:

  * :none - No notifications are sent
  * :rising - Send a notification when the pin changes from 0 to 1
  * :falling - Send a notification when the pin changes from 1 to 0
  * :both - Send a notification on all changes

  Available Options:
  * `suppress_glitches` - It is possible that the pin transitions to a value
  and back by the time that Circuits GPIO gets to process it. This controls
  whether a notification is sent. Set this to `false` to receive notifications.
  * `receiver` - Process which should receive the notifications.
  Defaults to the calling process (`self()`)

  Notifications look like:

  ```
  {:circuits_gpio, pin_number, timestamp, value}
  ```

  Where `pin_number` is the pin that changed values, `timestamp` is roughly when
  the transition occurred in nanoseconds since host system boot time,
  and `value` is the new value.

  NOTE: You will need to store the `Circuits.GPIO` reference somewhere (like
  your `GenServer`'s state) so that it doesn't get garbage collected. Event
  messages stop when it gets collected. If you only get one message and you are
  expecting more, this is likely the case.
  """
  @spec set_interrupts(Handle.t(), trigger(), interrupt_options()) :: :ok | {:error, atom()}
  defdelegate set_interrupts(handle, trigger, options \\ []), to: Handle

  @doc """
  Change the direction of the pin.
  """
  @spec set_direction(Handle.t(), pin_direction()) :: :ok | {:error, atom()}
  defdelegate set_direction(handle, pin_direction), to: Handle

  @doc """
  Enable or disable internal pull-up or pull-down resistor to GPIO pin
  """
  @spec set_pull_mode(Handle.t(), pull_mode()) :: :ok | {:error, atom()}
  defdelegate set_pull_mode(gpio, pull_mode), to: Handle

  @doc """
  Get the GPIO pin number
  """
  @spec pin(Handle.t()) :: pin_spec()
  def pin(handle) do
    info = Handle.info(handle)
    info.pin_spec
  end

  @doc """
  Return info about the low level GPIO interface

  This may be helpful when debugging issues.
  """
  @spec info(backend() | nil) :: map()
  def info(backend \\ nil)

  def info(nil), do: info(default_backend())
  def info({backend, _options}), do: backend.info()

  defp default_backend() do
    case Application.get_env(:circuits_gpio, :default_backend) do
      nil -> {Circuits.GPIO.NilBackend, []}
      m when is_atom(m) -> {m, []}
      {m, o} = value when is_atom(m) and is_list(o) -> value
    end
  end
end