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.Nif

  @typedoc "A GPIO pin number. See your device's documentation for how these connect to wires"
  @type pin_number :: 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_option :: {:initial_value, value() | :not_set} | {:pull_mode, pull_mode()}

  # 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_number(), pin_direction(), [open_option()]) ::
          {:ok, reference()} | {:error, atom()}
  def open(pin_number, pin_direction, options \\ []) do
    check_open_options(options)

    value = Keyword.get(options, :initial_value, :not_set)
    pull_mode = Keyword.get(options, :pull_mode, :not_set)

    Nif.open(pin_number, pin_direction, value, pull_mode)
  end

  defp check_open_options([]), do: :ok

  defp check_open_options([{:initial_value, value} | rest]) when value in [:not_set, 0, 1] do
    check_open_options(rest)
  end

  defp check_open_options([{:pull_mode, value} | rest])
       when value in [:not_set, :pullup, :pulldown, :none] do
    check_open_options(rest)
  end

  defp check_open_options([bad_option | _]) do
    raise ArgumentError.exception("Unsupported option to GPIO.open/3: #{inspect(bad_option)}")
  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(reference()) :: :ok
  def close(gpio) do
    Nif.close(gpio)
  end

  @doc """
  Read the current value on a pin.
  """
  @spec read(reference()) :: value()
  def read(gpio) do
    Nif.read(gpio)
  end

  @doc """
  Set the value of a pin. The pin should be configured to an output
  for this to work.
  """
  @spec write(reference(), value()) :: :ok
  def write(gpio, value) do
    Nif.write(gpio, value)
  end

  @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(reference(), trigger(), list()) :: :ok | {:error, atom()}
  def set_interrupts(gpio, trigger, opts \\ []) do
    suppress_glitches = Keyword.get(opts, :suppress_glitches, true)

    receiver =
      case Keyword.get(opts, :receiver) do
        pid when is_pid(pid) -> pid
        name when is_atom(name) -> Process.whereis(name) || self()
        _ -> self()
      end

    Nif.set_interrupts(gpio, trigger, suppress_glitches, receiver)
  end

  @doc """
  Change the direction of the pin.
  """
  @spec set_direction(reference(), pin_direction()) :: :ok | {:error, atom()}
  def set_direction(gpio, pin_direction) do
    Nif.set_direction(gpio, pin_direction)
  end

  @doc """
  Enable or disable internal pull-up or pull-down resistor to GPIO pin
  """
  @spec set_pull_mode(reference(), pull_mode()) :: :ok | {:error, atom()}
  def set_pull_mode(gpio, pull_mode) do
    Nif.set_pull_mode(gpio, pull_mode)
  end

  @doc """
  Get the GPIO pin number
  """
  @spec pin(reference) :: pin_number
  def pin(gpio) do
    Nif.pin(gpio)
  end

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

  This may be helpful when debugging issues.
  """
  @spec info() :: map()
  defdelegate info(), to: Nif
end