Packages
circuits_gpio
2.3.0
2.3.0
2.2.0
2.1.3
2.1.2
retired
2.1.1
retired
2.1.0
retired
2.0.2
retired
2.0.1
retired
2.0.0
retired
2.0.0-pre.6
retired
2.0.0-pre.5
retired
2.0.0-pre.4
retired
2.0.0-pre.3
retired
2.0.0-pre.2
retired
2.0.0-pre.1
retired
2.0.0-pre.0
retired
1.2.2
1.2.1
1.2.0
1.1.0
1.0.1
1.0.0
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.1
0.3.0
0.2.0
0.1.0
Use GPIOs in Elixir
Current section
Files
Jump to
Current section
Files
lib/gpio/cdev.ex
# 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, :locations]
@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)
drive_mode = Keyword.fetch!(options, :drive_mode)
# A single GPIO is just a group of one. All lines in a group must resolve to
# the same controller since the cdev backend requests them together.
specs = List.wrap(gpio_spec)
with {:ok, controller, offsets} <- resolve_group(specs, options),
{:ok, ref} <-
Nif.open(gpio_spec, {controller, offsets}, direction, value, pull_mode, drive_mode) do
locations = Enum.map(offsets, &{controller, &1})
{:ok, %__MODULE__{ref: ref, locations: locations}}
end
end
@impl Backend
def force_close(gpio_spec, options) do
with {:ok, location} <- find_location(gpio_spec, options) do
Nif.force_close(resolve_gpiochip(location))
end
end
defp resolve_group(specs, options) do
resolved =
Enum.reduce_while(specs, {:ok, []}, fn spec, {:ok, acc} ->
case find_location(spec, options) do
{:ok, location} -> {:cont, {:ok, [resolve_gpiochip(location) | acc]}}
error -> {:halt, error}
end
end)
with {:ok, reversed_locations} <- resolved do
locations = Enum.reverse(reversed_locations)
controllers = locations |> Enum.map(&elem(&1, 0)) |> Enum.uniq()
offsets = Enum.map(locations, &elem(&1, 1))
cond do
controllers == [] -> {:error, :no_gpios}
match?([_, _ | _], controllers) -> {:error, :multiple_controllers}
offsets != Enum.uniq(offsets) -> {:error, :duplicate_lines}
true -> {:ok, hd(controllers), offsets}
end
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 status(%Circuits.GPIO.CDev{locations: [location]}) do
Nif.status(location)
end
def status(%Circuits.GPIO.CDev{}) do
{:error, :group_handle}
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_drive_mode(%Circuits.GPIO.CDev{ref: ref}, drive_mode) do
Nif.set_drive_mode(ref, drive_mode)
end
@impl Handle
def set_interrupts(%Circuits.GPIO.CDev{ref: ref}, trigger, options) do
suppress_glitches = Keyword.get(options, :suppress_glitches, true)
Nif.set_interrupts(ref, trigger, suppress_glitches, resolve_receiver(options))
end
@impl Handle
def subscribe(%Circuits.GPIO.CDev{ref: ref}, options) do
notify_id = Keyword.get(options, :tag) || make_ref()
trigger = Keyword.get(options, :trigger) || :both
case Nif.subscribe(ref, notify_id, trigger, resolve_receiver(options)) do
:ok -> {:ok, notify_id}
error -> error
end
end
@impl Handle
def unsubscribe(%Circuits.GPIO.CDev{ref: ref}) do
Nif.unsubscribe(ref)
end
@impl Handle
def close(%Circuits.GPIO.CDev{ref: ref}) do
Nif.close(ref)
end
defp resolve_receiver(options) do
case Keyword.get(options, :receiver) do
pid when is_pid(pid) -> pid
name when is_atom(name) and not is_nil(name) -> Process.whereis(name) || self()
_ -> self()
end
end
end
end