lib/ccs811.ex

defmodule Ccs811 do
  alias Circuits.I2C
  alias Ccs811.Registries

  use Bitwise

  Registries.all()
  |> Enum.filter(fn {_, %{read: read}} -> read end)
  |> Enum.each(fn {key, %{address: address, bytes: bytes}} ->
    def unquote(:"read_#{key}")() do
      with {:ok, data} <- read_registry(unquote(address), unquote(bytes)) do
        translate(data, unquote(key))
      end
    end
  end)

  Registries.all()
  |> Enum.filter(fn {_, %{write: write}} -> write end)
  |> Enum.each(fn {key, %{address: address, bytes: bytes}} ->
    def unquote(:"write_#{key}")(data) when byte_size(data) == unquote(bytes) do
      write_registry(unquote(address), data)
    end
  end)

  @doc """
  Initializes the registries repository and the sensors,
  must be invoked before starting using the sensor

  It supports additional arguments:

  `:slave_address` - slave address of the sensor, default: 0x5A
  """
  def initialize(args \\ []) do
    :ok = Registries.init(args)
    app_start()
    set_meas_mode(:mode_1)

    :ok
  end

  defdelegate start_polling(args), to: Ccs811.Telemetry

  def app_verify(), do: write_registry(Registries.verify())
  def app_start(), do: write_registry(Registries.start())

  # MEAS - Measurement and Conditions
  def set_meas_mode(:mode_0), do: write_meas_mode(<<0>>)
  def set_meas_mode(:mode_1), do: write_meas_mode(<<16>>)
  def set_meas_mode(:mode_2), do: write_meas_mode(<<32>>)
  def set_meas_mode(:mode_3), do: write_meas_mode(<<48>>)
  def set_meas_mode(:mode_4), do: write_meas_mode(<<64>>)

  def reset, do: write_sw_reset(<<11, 229, 72, 138>>)

  def translate(<<data>>, :status) do
    %{
      fw_mode: data |> bit_mask_to_boolean(128),
      app_valid: data |> bit_mask_to_boolean(16),
      data_ready: data |> bit_mask_to_boolean(8),
      error: data |> bit_mask_to_boolean(1)
    }
  end

  def translate(<<data>>, :meas_mode) do
    drive_mode =
      case data &&& 112 do
        0 -> :mode_0
        16 -> :mode_1
        32 -> :mode_2
        48 -> :mode_3
        64 -> :mode_4
        _ -> :reserved
      end

    %{
      drive_mode: drive_mode,
      int_threshold: data |> bit_mask_to_boolean(4),
      int_data_ready: data |> bit_mask_to_boolean(8)
    }
  end

  def translate(<<data>>, :error_id) do
    %{
      write_reg_invalid: data |> bit_mask_to_boolean(1),
      red_reg_invalid: data |> bit_mask_to_boolean(2),
      measmode_invalid: data |> bit_mask_to_boolean(4),
      max_resistance: data |> bit_mask_to_boolean(8),
      heater_fault: data |> bit_mask_to_boolean(16),
      heater_supply: data |> bit_mask_to_boolean(32)
    }
  end

  def translate(
        <<eco2_high_byte, eco2_low_byte, tvoc_high_byte, tvoc_low_byte, status, error_id,
          _raw_data1, _raw_data2>>,
        :alg_result_data
      ) do
    <<eco2::16>> = <<eco2_high_byte, eco2_low_byte>>
    <<tvoc::16>> = <<tvoc_high_byte, tvoc_low_byte>>

    %{
      status: translate(<<status>>, :status),
      error_id: translate(<<error_id>>, :error_id),
      eco2: eco2,
      tvoc: tvoc
    }
  end

  def translate(<<major::4, minor::4, trivial>>, :fw_app_version) do
    "#{major}.#{minor}.#{trivial}"
  end

  def translate(<<data>>, _), do: data

  defp bit_mask_to_boolean(value, mask) do
    (value &&& mask) > 0
  end

  def read_registry(address, bytes) do
    open()
    |> I2C.write_read(Registries.slave_address(), [address], bytes)
  end

  def write_registry(address, data) do
    open()
    |> I2C.write(Registries.slave_address(), [address, data])
  end

  def write_registry(address) do
    open()
    |> I2C.write(Registries.slave_address(), [address])
  end

  defp open do
    with {:ok, bus_name} <- get_i2c_bus_name(),
         {:ok, ref} <- I2C.open(bus_name) do
      ref
    else
      {:error, :no_bus_names} ->
        raise "No I2C bus names found, check https://github.com/elixir-circuits/circuits_i2c#how-do-i-debug for info"

      {:error, error} ->
        raise "Unable to open I2C bus - #{inspect(error)}"
    end
  end

  defp get_i2c_bus_name do
    case Circuits.I2C.bus_names() do
      [] -> {:error, :no_bus_names}
      [first_available_bus_name | _] -> {:ok, first_available_bus_name}
    end
  end
end