lib/wafer/registers.ex

defmodule Wafer.Registers do
  @moduledoc """
  This module provides helpful macros for specifying the registers used to
  communicate with your device.

  This can be a massive time saver, and means you can basically just copy them
  straight out of the datasheet.

  See the documentation for `defregister/4` for more information.
  """
  alias Wafer.Chip
  alias Wafer.Conn

  @type register_name :: atom
  @type access_mode :: :ro | :rw | :wo
  @type bytes :: non_neg_integer

  @doc false
  defmacro __using__(_opts) do
    quote do
      import Wafer.Registers
    end
  end

  @doc ~S"""
  Define functions for interacting with a device register.

  ## Parameters
    - `name` - name of the register.
    - `register_address` - the address of the register.
    - `mode` the access mode of the register.
    - `bytes` the number of bytes in the register.

  ## Examples

  ### Read-only registers

  Define a read-only register named `status` at address `0x03` which is a single
  byte wide:

      iex> defregister(:status, 0x03, :ro, 1)

  This will define the following function along with documentation and
  typespecs:

  ```elixir
  def read_status(conn), do: Chip.read_register(conn, 0x03, 1)
  ```

  ### Write-only registers

  Define a write-only register named `int_en` at address `0x02` which is 2 bytes
  wide:

      iex> defregister(:int_en, 0x02, :wo, 2)

  This will define the following functions along with documentation and
  typespecs:

  ```elixir
  def write_int_en(conn, data)
      when is_binary(data) and byte_size(data) == 2,
      do: Chip.write_register(conn, 0x2, data)

  def write_int_en(_conn, data),
      do: {:error, "Argument error: #{inspect(data)}"}
  ```

  ### Read-write registers.

  Define a read-write register named `config` at address `0x01`.

      iex> defregister(:config, 0x01, :rw, 1)

  In addition to defining `read_config/1` and `write_config/2` as per the
  examples above it will also generate the following functions along with
  documentation and typespecs:

  ```elixir
  def swap_config(conn, data)
      when is_binary(data) and byte_size(data) == 1,
      do: Chip.swap_register(conn, 0x01, data)

  def swap_config(_conn, data),
      do: {:error, "Argument error: #{inspect(data)}"}

  def update_config(conn, callback)
      when is_function(callback, 1) do
        with {:ok, data} <- Chip.read_register(conn, 0x01, 1),
             new_data when is_binary(new_data) and byte_size(new_data) == 1 <- callback.(data),
             {:ok, conn} <- Chip.write_regsiter(conn, 0x01, new_data),
             do: {:ok, conn}
      end

  def update_config(_conn, _callback),
      do: {:error, "Argument error: callback should be an arity 1 function"}
  ```

  """
  @spec defregister(atom, non_neg_integer, :ro | :rw | :wo, non_neg_integer) :: Macro.t()
  defmacro defregister(name, register_address, :ro, bytes)
           when is_atom(name) and is_integer(register_address) and register_address >= 0 and
                  is_integer(bytes) and bytes >= 0 do
    empty_bytes = 1..bytes |> Enum.map_join(", ", fn _ -> 0 end)

    quote do
      @doc """
      Read the contents of the `#{unquote(name)}` register.

      ## Example

          iex> read_#{unquote(name)}(conn)
          {:ok, <<#{unquote(empty_bytes)}>>}
      """
      @spec unquote(:"read_#{name}")(Conn.t()) :: {:ok, binary} | {:error, reason :: any}
      def unquote(:"read_#{name}")(conn),
        do: Chip.read_register(conn, unquote(register_address), unquote(bytes))
    end
  end

  defmacro defregister(name, register_address, :wo, bytes)
           when is_atom(name) and is_integer(register_address) and register_address >= 0 and
                  is_integer(bytes) and bytes >= 0 do
    empty_bytes = 1..bytes |> Enum.map_join(", ", fn _ -> 0 end)

    quote do
      @doc """
      Write new contents to the `#{unquote(name)}` register.

      ## Example

          iex> write_#{unquote(name)}(conn, <<#{unquote(empty_bytes)}>>)
          {:ok, _conn}
      """
      @spec unquote(:"write_#{name}")(Conn.t(), data :: binary) ::
              {:ok, Conn.t()} | {:error, reason :: any}
      def unquote(:"write_#{name}")(conn, data)
          when is_binary(data) and byte_size(data) == unquote(bytes),
          do: Chip.write_register(conn, unquote(register_address), data)

      def unquote(:"write_#{name}")(_conn, data), do: {:error, "Argument error: #{inspect(data)}"}
    end
  end

  # credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity
  defmacro defregister(name, register_address, :rw, bytes)
           when is_atom(name) and is_integer(register_address) and register_address >= 0 and
                  is_integer(bytes) and bytes >= 0 do
    empty_bytes = 1..bytes |> Enum.map_join(", ", fn _ -> 0 end)
    bits = bytes * 8

    quote do
      @doc """
      Read the contents of the `#{unquote(name)}` register.

      ## Example

          iex> read_#{unquote(name)}(conn)
          {:ok, <<#{unquote(empty_bytes)}>>}
      """
      @spec unquote(:"read_#{name}")(Conn.t()) :: {:ok, binary} | {:error, reason :: any}
      def unquote(:"read_#{name}")(conn),
        do: Chip.read_register(conn, unquote(register_address), unquote(bytes))

      @doc """
      Write new contents to the `#{unquote(name)}` register.

      ## Example

          iex> write_#{unquote(name)}(conn, <<#{unquote(empty_bytes)}>>)
          {:ok, _conn}
      """
      @spec unquote(:"write_#{name}")(Conn.t(), data :: binary) ::
              {:ok, Conn.t()} | {:error, reason :: any}
      def unquote(:"write_#{name}")(conn, data)
          when is_binary(data) and byte_size(data) == unquote(bytes),
          do: Chip.write_register(conn, unquote(register_address), data)

      def unquote(:"write_#{name}")(_conn, data), do: {:error, "Argument error: #{inspect(data)}"}

      @doc """
      Swap the contents of the `#{unquote(name)}` register.

      Reads the contents of the register, then replaces it, returning the
      previous contents.  Some drivers may implement this atomically.

      ## Example

          iex> swap_#{unquote(name)}(conn, <<#{unquote(empty_bytes)}>>)
          {:ok, <<#{unquote(empty_bytes)}>>, _conn}
      """
      @spec unquote(:"swap_#{name}")(Conn.t(), data :: binary) ::
              {:ok, Conn.t()} | {:error, reason :: any}
      def unquote(:"swap_#{name}")(conn, data)
          when is_binary(data) and byte_size(data) == unquote(bytes),
          do: Chip.swap_register(conn, unquote(register_address), data)

      def unquote(:"swap_#{name}")(_conn, data), do: {:error, "Argument error: #{inspect(data)}"}

      @doc """
      Update the contents of the `#{unquote(name)}` register using a
      transformation function.

      ## Example

          iex> transform = fn <<data::size(#{unquote(bits)})>> -> <<(data * 2)::size(#{unquote(bits)})>> end
          ...> update_#{unquote(name)}(conn, transform)
          {:ok, _conn}
      """
      @spec unquote(:"update_#{name}")(
              Conn.t(),
              (<<_::_*unquote(bits)>> -> <<_::_*unquote(bits)>>)
            ) :: {:ok, Conn.t()} | {:error, reason :: any}
      def unquote(:"update_#{name}")(conn, callback) when is_function(callback, 1) do
        with {:ok, old_data} <-
               Chip.read_register(conn, unquote(register_address), unquote(bytes)),
             new_data when is_binary(new_data) and byte_size(new_data) == unquote(bytes) <-
               callback.(old_data),
             do: Chip.write_register(conn, unquote(register_address), new_data)
      end

      def unquote(:"update_#{name}")(_conn, _callback),
        do: {:error, "Argument error: callback should be an arity 1 function"}
    end
  end

  @doc """
  Define functions for interacting with a device register with common defaults.

  ## Examples

  When specified with an access mode, assumes a 1 byte register:

      iex> defregister(:status, 0x03, :ro)

  When specified with a byte size, assumes a `:rw` register:

      iex> defregister(:config, 0x02, 2)

  """
  @spec defregister(atom, non_neg_integer, :ro | :rw | :wo | non_neg_integer) :: Macro.t()
  defmacro defregister(name, register_address, mode) when mode in ~w[ro rw wo]a do
    quote do
      defregister(unquote(name), unquote(register_address), unquote(mode), 1)
    end
  end

  defmacro defregister(name, register_address, bytes) when is_integer(bytes) and bytes >= 0 do
    quote do
      defregister(unquote(name), unquote(register_address), :rw, unquote(bytes))
    end
  end
end