lib/gpio/cdev.ex

# SPDX-FileCopyrightText: 2023 Frank Hunleth, Connor Rigby
#
# SPDX-License-Identifier: Apache-2.0

defmodule Circuits.GPIO.CDev do
  @moduledoc """
  Circuits.GPIO backend that uses the Linux CDev for controlling GPIOs

  This is the default on Linux and Nerves. Nothing needs to be done to
  use it on those platforms. If you need to be explicit, here's the
  configuration to force it:

  ```elixir
  config :circuits_gpio, default_backend: Circuits.GPIO.CDev
  ```

  It takes one option, `:test`, that can be set to `true` to compile
  the stub implementation that can be useful for testing.

  ```elixir
  config :circuits_gpio, default_backend: {Circuits.GPIO.CDev, test: true}
  ```
  """
  @behaviour Circuits.GPIO.Backend

  alias Circuits.GPIO.Backend
  alias Circuits.GPIO.Handle
  alias Circuits.GPIO.Line
  alias Circuits.GPIO.Nif

  defstruct [:ref]

  @impl Backend
  def enumerate() do
    Nif.enumerate()
  end

  defp normalize_gpio_spec(number) when is_integer(number) do
    info = enumerate() |> Enum.at(number)

    if info, do: {:ok, info.gpio_spec}, else: {:error, :not_found}
  end

  defp normalize_gpio_spec(line_label) when is_binary(line_label) do
    spec =
      Enum.find_value(enumerate(), fn
        %Line{gpio_spec: spec, label: {_chip_label, ^line_label}} -> spec
        _ -> false
      end)

    if spec, do: {:ok, spec}, else: {:error, :not_found}
  end

  defp normalize_gpio_spec({chip_label, line_label})
       when is_binary(chip_label) and is_binary(line_label) do
    spec =
      Enum.find_value(enumerate(), fn
        %Line{gpio_spec: spec, label: {^chip_label, ^line_label}} -> spec
        _ -> false
      end)

    if spec, do: {:ok, spec}, else: {:error, :not_found}
  end

  defp normalize_gpio_spec({controller, line}) when is_binary(controller) and is_integer(line) do
    {:ok, {controller, line}}
  end

  defp resolve_gpiochip({controller, line}) do
    in_slash_dev = Path.expand(controller, "/dev")

    if File.exists?(in_slash_dev),
      do: {in_slash_dev, line},
      else: {controller, line}
  end

  @impl Backend
  def open(gpio_spec, direction, options) do
    value = Keyword.fetch!(options, :initial_value)
    pull_mode = Keyword.fetch!(options, :pull_mode)

    with {:ok, normalized_spec} <- normalize_gpio_spec(gpio_spec),
         resolved_spec = resolve_gpiochip(normalized_spec),
         {:ok, ref} <- Nif.open(gpio_spec, resolved_spec, direction, value, pull_mode) do
      {:ok, %__MODULE__{ref: ref}}
    end
  end

  @impl Backend
  def info() do
    Nif.info() |> Map.put(:name, __MODULE__)
  end

  defimpl Handle do
    @impl Handle
    def read(%Circuits.GPIO.CDev{ref: ref}) do
      Nif.read(ref)
    end

    @impl Handle
    def write(%Circuits.GPIO.CDev{ref: ref}, value) do
      Nif.write(ref, value)
    end

    @impl Handle
    def set_direction(%Circuits.GPIO.CDev{ref: ref}, direction) do
      Nif.set_direction(ref, direction)
    end

    @impl Handle
    def set_pull_mode(%Circuits.GPIO.CDev{ref: ref}, pull_mode) do
      Nif.set_pull_mode(ref, pull_mode)
    end

    @impl Handle
    def set_interrupts(%Circuits.GPIO.CDev{ref: ref}, trigger, options) do
      suppress_glitches = Keyword.get(options, :suppress_glitches, true)

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

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

    @impl Handle
    def close(%Circuits.GPIO.CDev{ref: ref}) do
      Nif.close(ref)
    end

    @impl Handle
    def info(%Circuits.GPIO.CDev{ref: ref}) do
      %{gpio_spec: Nif.gpio_spec(ref), pin_number: Nif.pin_number(ref)}
    end
  end
end