lib/wafer/drivers/elixir_ale/i2c.ex

defmodule Wafer.Driver.ElixirALE.I2C do
  defstruct ~w[address bus pid]a
  alias Wafer.Driver.ElixirALE.I2C.Wrapper
  alias Wafer.I2C
  import Wafer.Guards

  @moduledoc """
  A connection to a chip via ElixirALE's I2C driver.

  Implements the `Wafer.Conn` behaviour as well as the `Wafer.Chip` and `Wafer.I2C` protocols.
  """

  @type t :: %__MODULE__{address: I2C.address(), bus: binary, pid: pid}

  @type options :: [option]
  @type option :: {:bus_name, binary} | {:address, I2C.address()}

  @doc """
  Acquire a connection to a peripheral using the ElixirALE I2C driver on the
  specified bus and address.
  """
  @spec acquire(options) :: {:ok, t} | {:error, reason :: any}
  def acquire(opts) when is_list(opts) do
    with {:ok, bus} when is_binary(bus) <- Keyword.fetch(opts, :bus_name),
         {:ok, address} when is_i2c_address(address) <- Keyword.fetch(opts, :address),
         {:ok, pid} <- Wrapper.start_link(bus, address),
         devices when is_list(devices) <- Wrapper.detect_devices(pid),
         true <- Keyword.get(opts, :force, false) || Enum.member?(devices, address) do
      {:ok, %__MODULE__{bus: bus, address: address, pid: pid}}
    else
      false ->
        {:error, "No device detected at address. Pass `force: true` to override."}

      :error ->
        {:error, "ElixirALE.I2C requires both `bus_name` and `address` options."}

      {:error, reason} ->
        {:error, reason}
    end
  end
end

defimpl Wafer.Release, for: Wafer.Driver.ElixirALE.I2C do
  alias Wafer.Driver.ElixirALE.I2C.Wrapper
  alias Wafer.Driver.ElixirALE.I2C

  @doc """
  Release all resources associated with this I2C device.
  """
  @spec release(I2C.t()) :: :ok | {:error, reason :: any}
  def release(%I2C{pid: pid} = _conn) when is_pid(pid), do: Wrapper.release(pid)
end

defimpl Wafer.Chip, for: Wafer.Driver.ElixirALE.I2C do
  alias Wafer.Driver.ElixirALE.I2C.Wrapper
  import Wafer.Guards

  def read_register(%{pid: pid}, register_address, bytes)
      when is_pid(pid) and is_register_address(register_address) and is_byte_size(bytes) do
    case Wrapper.write_read(pid, <<register_address>>, bytes) do
      data when is_binary(data) and byte_size(data) == bytes -> {:ok, data}
      {:error, reason} -> {:error, reason}
      other -> {:error, "Invalid response from driver: #{inspect(other)}"}
    end
  end

  def read_register(_conn, _register_address, _bytes), do: {:error, "Invalid argument"}

  def write_register(%{pid: pid} = conn, register_address, data)
      when is_pid(pid) and is_register_address(register_address) and is_binary(data) do
    with :ok <- Wrapper.write(pid, <<register_address, data::binary>>), do: {:ok, conn}
  end

  def write_register(_conn, _register_address, _data), do: {:error, "Invalid argument"}

  def swap_register(conn, register_address, data)
      when is_register_address(register_address) and is_binary(data) do
    with {:ok, old_data} <- read_register(conn, register_address, byte_size(data)),
         {:ok, conn} <- write_register(conn, register_address, data) do
      {:ok, old_data, conn}
    end
  end

  def swap_register(_conn, _register_address, _data), do: {:error, "Invalid argument"}
end

defimpl Wafer.I2C, for: Wafer.Driver.ElixirALE.I2C do
  import Wafer.Guards
  alias Wafer.Driver.ElixirALE.I2C.Wrapper

  def read(%{pid: pid}, bytes, options \\ [])
      when is_pid(pid) and is_byte_size(bytes) and is_list(options) do
    case Wrapper.read(pid, bytes) do
      data when is_binary(data) and byte_size(data) == bytes -> {:ok, data}
      {:error, reason} -> {:error, reason}
      other -> {:error, "Invalid response from driver: #{inspect(other)}"}
    end
  end

  def write(%{pid: pid} = conn, data, options \\ [])
      when is_pid(pid) and is_binary(data) and is_list(options) do
    with :ok <- Wrapper.write(pid, data), do: {:ok, conn}
  end

  def write_read(%{pid: pid} = conn, data, bytes, options \\ [])
      when is_pid(pid) and is_binary(data) and is_byte_size(bytes) and is_list(options) do
    case Wrapper.write_read(pid, data, bytes) do
      data when is_binary(data) and byte_size(data) == bytes -> {:ok, data, conn}
      {:error, reason} -> {:error, reason}
      other -> {:error, "Invalid response from driver: #{inspect(other)}"}
    end
  end

  def detect_devices(%{pid: pid}) do
    case Wrapper.detect_devices(pid) do
      devices when is_list(devices) -> {:ok, devices}
      {:error, reason} -> {:error, reason}
      other -> {:error, "Invalid response from driver: #{inspect(other)}"}
    end
  end
end

defimpl Wafer.DeviceID, for: Wafer.Driver.ElixirALE.I2C do
  def id(%{address: address, bus: bus}), do: {Wafer.Driver.ElixirALE.I2C, bus, address}
end