# 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