lib/apds9960/sensor.ex

defmodule APDS9960.Sensor do
  @moduledoc "The APDS9960 sensor."

  alias APDS9960.{Comm, Sensor, Transport}

  @i2c_address 0x39

  use TypedStruct

  typedstruct do
    field(:transport, Transport.t(), enforce: true)
  end

  @typedoc "The APDS9960 sensor option"
  @type option() :: [
          {:bus_name, binary}
          | {:reset, boolean}
          | {:set_defaults, boolean}
        ]

  @type engine :: :color | :als | :proximity | :gesture

  @type gesture_direction :: :up | :down | :left | :right

  @doc "Initializes the I2C bus and sensor."
  @spec init([option]) :: t()
  def init(opts \\ []) do
    bus_name = Access.get(opts, :bus_name, "i2c-1")
    reset = Access.get(opts, :reset, true)
    set_defaults = Access.get(opts, :set_defaults, true)

    sensor = %Sensor{
      transport: Transport.new(bus_name, @i2c_address)
    }

    :ok = ensure_connected!(sensor)

    if reset, do: reset!(sensor)
    if set_defaults, do: set_defaults!(sensor)

    %Sensor{} = sensor
  end

  @spec ensure_connected!(Sensor.t()) :: :ok
  defp ensure_connected!(%Sensor{transport: i2c}) do
    true = Comm.connected?(i2c)
    :ok
  end

  @spec reset!(Sensor.t()) :: :ok
  def reset!(%Sensor{transport: i2c}) do
    # Disable prox, gesture, and color engines
    :ok = Comm.set_enable(i2c, gesture: 0, proximity: 0, als: 0)

    # Reset basic config registers to power-on defaults
    :ok = Comm.set_proximity_threshold(i2c, low: 0, high: 0)
    :ok = Comm.set_interrupt_persistence(i2c, <<0>>)
    :ok = Comm.set_gesture_proximity_threshold(i2c, enter: 0, exit: 0)
    :ok = Comm.set_gesture_conf1(i2c, <<0>>)
    :ok = Comm.set_gesture_conf2(i2c, <<0>>)
    :ok = Comm.set_gesture_conf4(i2c, <<0>>)
    :ok = Comm.set_gesture_pulse(i2c, <<0>>)
    :ok = Comm.set_adc_integration_time(i2c, <<255>>)
    :ok = Comm.set_control(i2c, als_and_color_gain: 1)

    # Clear all non-gesture interrupts
    :ok = Comm.clear_all_non_gesture_interrupts(i2c)

    # Disable sensor and all functions/interrupts
    :ok = Comm.set_enable(i2c, <<0>>)
    :ok = Process.sleep(25)

    # Re-enable sensor and wait 10ms for the power on delay to finish
    :ok = Comm.set_enable(i2c, power: 1)
    :ok = Process.sleep(10)

    :ok
  end

  @spec set_defaults!(Sensor.t()) :: :ok
  def set_defaults!(%Sensor{transport: i2c}) do
    # Trigger proximity interrupt at >= 5, PPERS: 4 cycles
    :ok = Comm.set_proximity_threshold(i2c, low: 0, high: 5)
    :ok = Comm.set_interrupt_persistence(i2c, proximity: 4)

    # Enter gesture engine at >= 5 proximity counts
    # Exit gesture engine if all counts drop below 30
    :ok = Comm.set_gesture_proximity_threshold(i2c, enter: 5, exit: 30)

    # GEXPERS: 2 (4 cycles), GEXMSK: 0 (default) GFIFOTH: 2 (8 datasets)
    :ok =
      Comm.set_gesture_conf1(i2c,
        fifo_threshold: 2,
        exit_mask: 0,
        exit_persistence: 2
      )

    # GGAIN: 2 (4x), GLDRIVE: 0 (100 mA), GWTIME: 1 (2.8 ms)
    :ok =
      Comm.set_gesture_conf2(i2c,
        gain: 2,
        led_drive_strength: 0,
        wait_time: 1
      )

    # GPULSE: 5 (6 pulses), GPLEN: 2 (16 us)
    :ok =
      Comm.set_gesture_pulse(i2c,
        pulse_count: 5,
        pulse_length: 2
      )

    # ATIME: 0 (712ms color integration time, max count of 65535)
    :ok = Comm.set_adc_integration_time(i2c, <<0>>)

    # AGAIN: 1 (4x color gain)
    :ok = Comm.set_control(i2c, als_and_color_gain: 1)

    :ok
  end

  @doc "Enable an engine for a desired feature."
  @spec enable(Sensor.t(), engine) :: :ok
  def enable(%Sensor{transport: i2c}, :color), do: Comm.set_enable(i2c, als: 1)
  def enable(%Sensor{transport: i2c}, engine), do: Comm.set_enable(i2c, [{engine, 1}])
end