Skip to main content

lib/lis3dh/interrupts.ex

defmodule LIS3DH.Interrupts do
  @moduledoc """
  Encoding and decoding for the LIS3DH's inertial interrupt configuration
  (`INT1_CFG`, `INT1_THS`, `INT1_DURATION` and their `INT2_*` siblings) plus
  the per-pin routing bits in `CTRL_REG3` and `CTRL_REG6` and the
  latching/4D bits in `CTRL_REG5`.

  The chip has two physical interrupt pins, INT1 and INT2, each driven by a
  configurable mix of event sources:

    * inertial interrupts 1 / 2 (AOI engines reading `INT*_CFG`),
    * click detection,
    * data-ready for accelerometer (`ZYXDA`) or auxiliary ADC (`321DA`),
    * FIFO watermark / overrun (INT1 only — handled by `LIS3DH.Sampler`),
    * activity / boot (INT2 only).

  An inertial interrupt fires when the per-axis event flags in `INT*_CFG`
  combine according to the `AOI` / `6D` bits:

  ```text
  AOI  6D   Behaviour
   0   0   OR of the enabled axis events (e.g. wake-up / motion)
   0   1   6D movement recognition (entering a known zone)
   1   0   AND of the enabled axis events (e.g. free-fall)
   1   1   6D position recognition (currently in a known zone)
  ```

  Threshold and duration registers carry units that depend on the configured
  full-scale range and ODR; see `threshold_lsb_mg/1` and the helpers in
  `LIS3DH` for unit-aware wrappers.
  """

  import Bitwise

  alias LIS3DH.Config

  @typedoc "Which interrupt pin a configuration applies to."
  @type pin :: :int1 | :int2

  @typedoc """
  Combined AOI/6D field in `INT*_CFG`.

    * `:or` — OR of enabled axis events.
    * `:and` — AND of enabled axis events.
    * `:six_d_movement` — interrupt when orientation enters a known zone.
    * `:six_d_position` — interrupt while orientation is inside a known zone.
  """
  @type aoi_mode :: :or | :and | :six_d_movement | :six_d_position

  @typedoc """
  Per-axis-direction event flags. Each entry enables interrupt generation
  when the named axis crosses the configured threshold in the named
  direction.
  """
  @type axis_event ::
          :x_high | :x_low | :y_high | :y_low | :z_high | :z_low

  @typedoc """
  Decoded `INT*_SRC` flags. `:active` is the master IA bit; the per-axis
  fields report which axis-direction events fired during the latched window
  (or this read, for non-latched mode).
  """
  @type source_flags :: %{
          active: boolean,
          x_high: boolean,
          x_low: boolean,
          y_high: boolean,
          y_low: boolean,
          z_high: boolean,
          z_low: boolean
        }

  @aoi_codes %{
    or: {0, 0},
    six_d_movement: {0, 1},
    and: {1, 0},
    six_d_position: {1, 1}
  }
  @aoi_decodes Map.new(@aoi_codes, fn {k, v} -> {v, k} end)

  @event_bits %{
    x_low: 0,
    x_high: 1,
    y_low: 2,
    y_high: 3,
    z_low: 4,
    z_high: 5
  }

  @threshold_lsb %{2 => 16, 4 => 32, 8 => 62, 16 => 186}

  @doc """
  Encode an `INT*_CFG` byte from keyword options.

  ## Options

    * `:mode` — `t:aoi_mode/0` (default `:or`).
    * `:axes` — list of `t:axis_event/0` to enable (default `[]`).
  """
  @spec encode_int_cfg(keyword) :: <<_::8>>
  def encode_int_cfg(opts \\ []) when is_list(opts) do
    mode = Keyword.get(opts, :mode, :or)
    axes = Keyword.get(opts, :axes, [])

    {aoi, six_d} = lookup!(@aoi_codes, mode, :mode)

    axis_bits =
      Enum.reduce(axes, 0, fn event, acc ->
        bit = Map.fetch!(@event_bits, event)
        acc ||| 1 <<< bit
      end)

    <<aoi <<< 7 ||| six_d <<< 6 ||| axis_bits>>
  end

  @doc "Decode an `INT*_CFG` byte into a map of its fields."
  @spec decode_int_cfg(<<_::8>>) :: %{mode: aoi_mode, axes: [axis_event]}
  def decode_int_cfg(<<byte>>) do
    aoi = byte >>> 7 &&& 1
    six_d = byte >>> 6 &&& 1
    mode = lookup!(@aoi_decodes, {aoi, six_d}, :aoi_bits)

    axes =
      for {event, bit} <- Enum.sort_by(@event_bits, &elem(&1, 1)),
          (byte >>> bit &&& 1) == 1,
          do: event

    %{mode: mode, axes: axes}
  end

  @doc "Decode an `INT*_SRC` byte into a map of its fields."
  @spec decode_int_src(<<_::8>>) :: source_flags
  def decode_int_src(<<byte>>) do
    %{
      active: (byte >>> 6 &&& 1) == 1,
      z_high: (byte >>> 5 &&& 1) == 1,
      z_low: (byte >>> 4 &&& 1) == 1,
      y_high: (byte >>> 3 &&& 1) == 1,
      y_low: (byte >>> 2 &&& 1) == 1,
      x_high: (byte >>> 1 &&& 1) == 1,
      x_low: (byte &&& 1) == 1
    }
  end

  @doc """
  Returns the threshold register's LSB size in milli-g for the given full-
  scale range, per datasheet §8.23 / §8.27.
  """
  @spec threshold_lsb_mg(Config.range()) :: pos_integer
  def threshold_lsb_mg(range), do: Map.fetch!(@threshold_lsb, range)

  @doc """
  Encode a threshold in milli-g into a 7-bit `INT*_THS` register value for
  the given range. Rounds down. Clamps at the 7-bit maximum (127).
  """
  @spec encode_threshold!(non_neg_integer, Config.range()) :: <<_::8>>
  def encode_threshold!(threshold_mg, range)
      when is_integer(threshold_mg) and threshold_mg >= 0 do
    lsb = threshold_lsb_mg(range)
    raw = min(div(threshold_mg, lsb), 0x7F)
    <<raw>>
  end

  @doc """
  Encode a duration in ODR counts into a 7-bit `INT*_DURATION` register
  value. Each LSB is `1/ODR` (so at 100 Hz, count=1 ≈ 10 ms).
  """
  @spec encode_duration!(0..127) :: <<_::8>>
  def encode_duration!(count) when is_integer(count) and count in 0..127 do
    <<count>>
  end

  defp lookup!(map, key, field) do
    case Map.fetch(map, key) do
      {:ok, value} ->
        value

      :error ->
        raise ArgumentError,
              "invalid #{field}: #{inspect(key)} (valid values: #{inspect(Map.keys(map))})"
    end
  end
end