lib/nrf24.ex

defmodule Nrf24 do
  @moduledoc """

  Library for trasmitting and receiveing data with nRF24L01+
  transciever.

  It works, by default, in ShockBurst mode with ACK (auto
  acknowledgement) and CRC check enabled. However it can be configured
  to disable both.

  """

  use GenServer

  require Logger

  alias Nrf24.Transciever

  # Client

  @type init_options() :: [
          name: GenServer.name(),
          ce_pin: integer(),
          csn_pin: integer(),
          channel: integer(),
          crc_length: 0 | 1 | 2,
          speed: :low | :medium | :hi,
          pipes: [
            [
              pipe_no: integer(),
              address: integer(),
              payload_size: integer(),
              auto_ack: boolean()
            ]
          ]
        ]
  @doc """
  nRF24L01+ GenServer start_link options

  * `:name` - GenServer nome
  * `:bus_name` - SPI bus name (e.g. "spidev0.0", "spidev1.0", default: "spidev0.0")
  * `:ce_pin` - Rasbperry PI pin to which transciever's CE pin is connected
  * `:csn_pin` - Rasbperry PI pin to which transciever's CSN pin is connected
  * `:channel` - Frequency channel on which transciever will operate (default: 0x4c)
  * `:crc_length` - CRC length for transmitted data verification (values: 1, 2, default: 2)
  * `:speed` - Data transfer speed (values: low, med, high, default: med)

  Data speed values:

    * `:low` - 250Kbp
    * `:medium` - 1Mbps
    * `:high` - 2Mbps

  Pipe configuration options:

  * `:pipe_no` - Pipe number (values 0 to 5)
  * `:address` - For pipes 0 and 1 5-byte address and for other single byte address
  * `:payload_size` - Size of data that will be received through the pipe
  * `:auto_ack` - Turning auto-acknowledge on or off for the pipe (default: true)

  If pipes configuration is missing, default, factory set, valuse will be used.
  """
  @spec start_link(init_options()) :: GenServer.on_start()
  def start_link(args) do
    options = Keyword.take(args, [:name])
    GenServer.start_link(__MODULE__, args, options)
  end

  # Server

  @impl GenServer
  def init(args) do
    bus_name = Keyword.get(args, :bus_name, "spidev0.0")

    case Circuits.SPI.open(bus_name) do
      {:error, error} ->
        {:stop, error}

      {:ok, spi} ->
        ce_pin = Keyword.get(args, :ce_pin)
        csn_pin = Keyword.get(args, :csn_pin)
        channel = Keyword.get(args, :channel, 0x4C)
        crc_length = Keyword.get(args, :crc_length, 2)
        speed = Keyword.get(args, :speed, :medium)

        with {:ok, _} <- Transciever.set_channel(spi, channel),
             {:ok, _} <- Transciever.set_crc_length(spi, crc_length),
             {:ok, _} <- Transciever.set_speed(spi, speed) do
          Keyword.get(args, :pipes, [])
          |> Enum.each(fn pipe_opts ->
            pipe_no = Keyword.get(pipe_opts, :pipe_no)

            Transciever.set_pipe(spi, pipe_no, pipe_opts)
          end)

          state = %{
            spi: spi,
            ce_pin: ce_pin,
            csn_pin: csn_pin
          }

          {:ok, state}
        else
          {:error, error} -> {:stop, error}
        end
    end
  end

  @impl GenServer
  def handle_call({:set_channel, channel}, _from, state = %{spi: spi}) do
    if is_reference(spi) do
      {:reply, Transciever.set_channel(spi, channel), state}
    else
      {:reply, {:error, :spi_not_opened}}
    end
  end

  def handle_call({:set_crc_length, length}, _from, state = %{spi: spi}) do
    if is_reference(spi) do
      {:reply, Transciever.set_crc_length(spi, length), state}
    else
      {:reply, {:error, :spi_not_opened}}
    end
  end

  def handle_call(:set_receive_mode, _from, state = %{spi: spi}) do
    if is_reference(spi) do
      {:reply, Transciever.set_receive_mode(spi), state}
    else
      {:reply, {:error, :spi_not_opened}}
    end
  end

  def handle_call(:set_transmit_mode, _from, state = %{spi: spi}) do
    if is_reference(spi) do
      {:reply, Transciever.set_transmit_mode(spi), state}
    else
      {:reply, {:error, :spi_not_opened}}
    end
  end

  def handle_call(:power_on, _from, state = %{spi: spi}) do
    if is_reference(spi) do
      {:reply, Transciever.power_on(spi), state}
    else
      {:reply, {:error, :spi_not_opened}}
    end
  end

  def handle_call(:power_off, _from, state = %{spi: spi}) do
    if is_reference(spi) do
      {:reply, Transciever.power_off(spi), state}
    else
      {:reply, {:error, :spi_not_opened}}
    end
  end

  def handle_call({:set_speed, speed}, _from, state = %{spi: spi}) do
    if is_reference(spi) do
      {:reply, Transciever.set_speed(spi, speed), state}
    else
      {:reply, {:error, :spi_not_opened}}
    end
  end

  def handle_call({:set_pipe_address, pipe_no, address}, _from, state = %{spi: spi}) do
    if is_reference(spi) do
      {:reply, Transciever.set_pipe_address(spi, pipe_no, address), state}
    else
      {:reply, {:error, :spi_not_opened}}
    end
  end

  def handle_call({:set_trasmit_address, address}, _from, state = %{spi: spi}) do
    if is_reference(spi) do
      {:reply, Transciever.set_transmit_address(spi, address), state}
    else
      {:reply, {:error, :spi_not_opened}}
    end
  end

  def handle_call({:set_payload_size, pipe_no, payload_size}, _from, state = %{spi: spi}) do
    if is_reference(spi) do
      {:reply, Transciever.set_payload_size(spi, pipe_no, payload_size), state}
    else
      {:reply, {:error, :spi_not_opened}}
    end
  end

  def handle_call({:ack_on, pipe_no}, _from, state = %{spi: spi}) do
    if is_reference(spi) do
      {:reply, Transciever.ack(spi, pipe_no, true), state}
    else
      {:reply, {:error, :spi_not_opened}}
    end
  end

  def handle_call({:ack_off, pipe_no}, _from, state = %{spi: spi}) do
    if is_reference(spi) do
      {:reply, Transciever.ack(spi, pipe_no, false), state}
    else
      {:reply, {:error, :spi_not_opened}}
    end
  end

  def handle_call({:enable_pipe, pipe_no}, _from, state = %{spi: spi}) do
    if is_reference(spi) do
      {:reply, Transciever.enable_pipe(spi, pipe_no), state}
    else
      {:reply, {:error, :spi_not_opened}}
    end
  end

  def handle_call({:disable_pipe, pipe_no}, _from, state = %{spi: spi}) do
    if is_reference(spi) do
      {:reply, Transciever.disable_pipe(spi, pipe_no), state}
    else
      {:reply, {:error, :spi_not_opened}}
    end
  end

  def handle_call({:set_retransmit_delay, delay}, _from, state = %{spi: spi}) do
    if is_reference(spi) do
      {:reply, Transciever.set_retransmit_delay(spi, delay), state}
    else
      {:reply, {:error, :spi_not_opened}}
    end
  end

  def handle_call({:set_retransmit_count, count}, _from, state = %{spi: spi}) do
    if is_reference(spi) do
      {:reply, Transciever.set_retransmit_count(spi, count), state}
    else
      {:reply, {:error, :spi_not_opened}}
    end
  end

  def handle_call(:reset_status, _from, state = %{spi: spi}) do
    if is_reference(spi) do
      {:reply, Transciever.reset_status(spi), state}
    else
      {:reply, {:error, :spi_not_opened}}
    end
  end

  def handle_call({:set_pipe, pipe_no, options}, _from, state = %{spi: spi}) do
    if is_reference(spi) do
      {:reply, Transciever.set_pipe(spi, pipe_no, options), state}
    else
      {:reply, {:error, :spi_not_opened}}
    end
  end

  def handle_call(:start_listening, _from, state = %{spi: spi, ce_pin: ce_pin}) do
    if is_reference(spi) do
      {:reply, Transciever.start_listening(spi, ce_pin), state}
    else
      {:reply, {:error, :spi_not_opened}}
    end
  end

  def handle_call(:stop_listening, _from, state = %{spi: spi, ce_pin: ce_pin}) do
    if is_reference(spi) do
      {:reply, Transciever.stop_listening(spi, ce_pin), state}
    else
      {:reply, {:error, :spi_not_opened}}
    end
  end

  def handle_call(:reset_device, _from, state = %{spi: spi}) do
    if is_reference(spi) do
      {:reply, Transciever.reset(spi), state}
    else
      {:reply, {:error, :spi_not_opened}}
    end
  end

  def handle_call({:read_data, payload_size}, _from, state = %{spi: spi}) do
    if is_reference(spi) do
      {:reply, Transciever.read_data(spi, payload_size), state}
    else
      {:reply, {:error, :spi_not_opened}}
    end
  end

  def handle_call({:write_register, reg, value}, _from, state = %{spi: spi}) do
    if is_reference(spi) do
      {:reply, Transciever.write_register(spi, reg, value), state}
    else
      {:reply, {:error, :spi_not_opened}}
    end
  end

  def handle_call({:read_register, reg}, _from, state = %{spi: spi}) do
    if is_reference(spi) do
      {:reply, Transciever.read_register(spi, reg), state}
    else
      {:reply, {:error, :spi_not_opened}}
    end
  end

  def handle_call({:read_register, reg, bytes_no}, _from, state = %{spi: spi}) do
    if is_reference(spi) do
      {:reply, Transciever.read_register(spi, reg, bytes_no), state}
    else
      {:reply, {:error, :spi_not_opened}}
    end
  end

  @impl GenServer
  def handle_cast({:send, address, data}, state = %{spi: spi, csn_pin: csn_pin, ce_pin: ce_pin}) do
    with :ok <- Transciever.start_sending(spi, address),
         {:ok, ce} <- Transciever.send_data(spi, data, csn_pin, ce_pin) do
      Process.send_after(self(), {:stop_sending, ce}, 1)
    else
      {:error, error} ->
        Logger.info("Send data failed with error: #{inspect(error)}")

      _ ->
        Logger.info("Send data failed with unknown error")
    end

    {:noreply, state}
  end

  @impl GenServer
  def handle_info({:stop_sending, ce}, state) do
    Transciever.stop_sending(ce)

    {:noreply, state}
  end

  @impl GenServer
  def terminate(_reason, %{spi: spi}) do
    if is_reference(spi), do: Circuits.SPI.close(spi)
  end
end