lib/hts221/server.ex

defmodule HTS221.Server do
  @moduledoc """
  Server for setting up and using the HTS221

  When controlling the HTS221 there are few setup steps and other checks you
  may want to do. Also, to keep the transport layer working often times this
  will call for a GenServer. This server is meant to provide common
  functionality around setup and an expose a higher level API for application
  use.

  This process can be added to your supervision tree.

  ```
  def MyApp do
    use Application

    def start(_type, _args) do
      children = [
        ... children ...
        {HTS221.Server, transport: {HTS221.Transport.I2C, [bus_name: "i2c-1"]}
        ... children ...
      ]

      opts = [strategy: :one_for_one, name: MyApp.Supervisor]
      Supervisor.start(children, opts)
    end
  end
  ```

  If you a custom transport implementation then the `:transport` argument to
  this server will look different.

  If the HTS221 is not detected this server will log that it was not found and
  return `:ignore` on the `GenServer.init/1` callback. This is useful if your FW
  will run on devices with different hardware attached and don't want the device
  availability to crash your application supervisor.
  """

  use GenServer

  require Logger

  alias HTS221.{CTRLReg1, CTRLReg2, Transport}

  defmodule State do
    @moduledoc false

    alias HTS221.{Calibration, Transport}

    @type t() :: %__MODULE__{
            calibration: Calibration.t(),
            transport: Transport.t()
          }

    defstruct calibration: nil, transport: nil
  end

  @type name() :: any()

  @typedoc """
  Arguments to the `HTS221.Server`

    - `:transport` - the transport implementation module and optional arguments
    - `:name` - this is a named GenServer that either uses what you pass in or
      `HTS221.Server`
  """
  @type arg() ::
          {:transport, Transport.impl() | {Transport.impl(), [Transport.arg()]}} | {:name, name()}

  @type temperature_opt() :: {:scale, HTS221.scale()}

  @spec start_link([arg()]) :: GenServer.on_start()
  def start_link(args) do
    name = Keyword.get(args, :name, __MODULE__)
    GenServer.start_link(__MODULE__, args, name: name)
  end

  @doc """
  Read the temperature value

  By default this is read in celsius you can the `:scale` option to change the
  unit the temperature is measured in. See `HTS221.scale()` for more
  information.
  """
  @spec temperature(name(), [temperature_opt()]) :: {:ok, float()} | {:error, any()}
  def temperature(server, opts \\ []) do
    GenServer.call(server, {:temperature, opts})
  end

  @doc """
  Read the humidity level

  This is measured in percent.
  """
  @spec humidity(name()) :: {:ok, float()} | {:error, any()}
  def humidity(server) do
    GenServer.call(server, :humidity)
  end

  @doc """
  Get the transport the server is using

  This is useful if you need to debug the registers using the functions in
  `HTS221` module.
  """
  @spec transport(name()) :: Transport.t()
  def transport(server) do
    GenServer.call(server, :transport)
  end

  @impl GenServer
  def init(args) do
    {transport_impl, transport_args} = get_transport(args)

    with {:ok, transport} <- Transport.init(transport_impl, transport_args),
         :ok = reboot_registers(transport),
         {:ok, calibration} <- HTS221.read_calibration(transport),
         :ok <- setup_ctrl_reg1(transport) do
      {:ok, %State{calibration: calibration, transport: transport}}
    else
      {:error, :device_not_available} ->
        Logger.info("HTS221 not detected on device")
        :ignore

      error ->
        {:stop, error}
    end
  end

  @impl GenServer
  def handle_call({:temperature, opts}, _from, state) do
    %State{calibration: calibration, transport: transport} = state

    case HTS221.read_temperature(transport) do
      {:ok, temp} ->
        {:reply, {:ok, HTS221.calculate_temperature(temp, calibration, opts)}, state}

      error ->
        {:reply, error, state}
    end
  end

  def handle_call(:humidity, _from, state) do
    %State{calibration: calibration, transport: transport} = state

    case HTS221.read_humidity(transport) do
      {:ok, hum} ->
        {:reply, {:ok, HTS221.calculate_humidity(hum, calibration)}, state}

      error ->
        {:reply, error, state}
    end
  end

  def handle_call(:transport, _from, state) do
    {:reply, state.transport, state}
  end

  defp get_transport(args) do
    case Keyword.fetch!(args, :transport) do
      {_, _} = transport -> transport
      transport when is_atom(transport) -> {transport, []}
    end
  end

  defp reboot_registers(transport) do
    # Sometimes registers need to be rebooted to ensure
    # calibration is correctly being read from the non-volatile
    # memory.

    ctrl_reg2 = %CTRLReg2{
      boot: :reboot_memory
    }

    HTS221.write_register(transport, ctrl_reg2)
  end

  defp setup_ctrl_reg1(transport) do
    # setup HTS221 to continuously read datasets
    ctrl_reg1 = %CTRLReg1{
      power_mode: :active,
      block_data_update: :wait_for_reading,
      output_data_rate: :one_Hz
    }

    HTS221.write_register(transport, ctrl_reg1)
  end
end