# SPDX-FileCopyrightText: 2023 Connor Rigby
# SPDX-FileCopyrightText: 2023 Frank Hunleth
#
# 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.Nif
defstruct [:ref]
@impl Backend
def enumerate(options) do
cached = :persistent_term.get(__MODULE__, [])
if cached == [] or options[:force_enumeration] do
results = Nif.enumerate()
:persistent_term.put(__MODULE__, results)
results
else
cached
end
end
defp find_by_index(gpios, index), do: Enum.at(gpios, index)
defp find_by_tuple(gpios, {controller, label_or_index}) do
Enum.find(gpios, fn
%{location: {^controller, _}, label: ^label_or_index} -> true
%{controller: ^controller, label: ^label_or_index} -> true
%{location: {^controller, ^label_or_index}} -> true
%{controller: ^controller, location: {_, ^label_or_index}} -> true
_ -> false
end)
end
defp find_by_label(gpios, label) do
Enum.find(gpios, fn
%{label: ^label} -> true
_ -> false
end)
end
defp retry_find(options, find_fun) do
info =
find_fun.(enumerate(options)) ||
find_fun.(enumerate([{:force_enumeration, true} | options]))
if info, do: {:ok, info}, else: {:error, :not_found}
end
@impl Backend
def identifiers(number, options) when is_integer(number) and number >= 0 do
retry_find(options, &find_by_index(&1, number))
end
def identifiers(line_label, options) when is_binary(line_label) do
retry_find(options, &find_by_label(&1, line_label))
end
def identifiers(tuple_spec, options)
when is_tuple(tuple_spec) and tuple_size(tuple_spec) == 2 do
retry_find(options, &find_by_tuple(&1, tuple_spec))
end
def identifiers(_gpio_spec, _options) do
{:error, :not_found}
end
@impl Backend
def status(gpio_spec, options \\ []) do
with {:ok, location} <- find_location(gpio_spec, options) do
resolved_location = resolve_gpiochip(location)
Nif.status(resolved_location)
end
end
# Handle special case that doesn't require a lookup
defp find_location({"gpiochip" <> _, line} = gpio_spec, _options) when is_integer(line) do
{:ok, gpio_spec}
end
defp find_location(gpio_spec, options) do
with {:ok, ids} <- identifiers(gpio_spec, options) do
{:ok, ids.location}
end
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, location} <- find_location(gpio_spec, options),
resolved_location = resolve_gpiochip(location),
{:ok, ref} <- Nif.open(gpio_spec, resolved_location, direction, value, pull_mode) do
{:ok, %__MODULE__{ref: ref}}
end
end
@impl Backend
def backend_info() do
Nif.backend_info()
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
end
end