# `BMI323`

Driver for the Bosch BMI323 6-DoF inertial measurement unit.

Communicates over I²C (or any other [Wafer](https://hex.pm/packages/wafer)
transport that implements the `Wafer.I2C` protocol).

## Protocol notes

The BMI323 uses an unusual word-oriented register protocol:

  * Every register is 16 bits wide, little-endian.
  * Every read returns **two dummy bytes** before the payload; this driver
    strips them transparently.
  * Writes have no dummy bytes — just the 8-bit address followed by the
    16-bit value(s).
  * Multi-word reads and writes auto-increment the register address.

## Example

    {:ok, i2c} = Wafer.Driver.Circuits.I2C.acquire(bus_name: "i2c-1", address: 0x68)
    {:ok, bmi} = BMI323.acquire(conn: i2c)

# `acquire_option`

```elixir
@type acquire_option() ::
  {:conn, Wafer.Conn.t()}
  | {:soft_reset, boolean()}
  | {:verify_chip_id, boolean()}
```

# `axes`

```elixir
@type axes() :: %{x: float(), y: float(), z: float()}
```

# `chip_id`

```elixir
@type chip_id() :: byte()
```

# `imu_sample`

```elixir
@type imu_sample() :: %{
  accelerometer: axes(),
  gyroscope: axes(),
  temperature: float()
}
```

# `t`

```elixir
@type t() :: %BMI323{
  accelerometer_range: BMI323.Config.accelerometer_range() | nil,
  conn: Wafer.Conn.t(),
  gyroscope_range: BMI323.Config.gyroscope_range() | nil
}
```

# `acquire`

```elixir
@spec acquire([acquire_option()]) :: {:ok, t()} | {:error, term()}
```

Wrap an existing Wafer connection in a `BMI323` struct.

## Options

  * `:conn` (required) — a Wafer connection that implements the `Wafer.I2C`
    protocol, e.g. `Wafer.Driver.Circuits.I2C` or `Wafer.Driver.Fake`.
  * `:soft_reset` (default `false`) — when `true`, issue a soft reset
    immediately after wrapping the connection. See `soft_reset/1`.
  * `:verify_chip_id` (default `true`) — when `true`, read `CHIP_ID` and
    return `{:error, {:chip_id_mismatch, got: byte, expected: 0x43}}` if the
    device does not identify as a BMI323.

# `chip_id`

```elixir
@spec chip_id(t()) :: {:ok, chip_id()} | {:error, term()}
```

Read the device's `CHIP_ID` register, returning the 7-bit identifier
(high byte of the 16-bit register is reserved and discarded).

# `configure_accelerometer`

```elixir
@spec configure_accelerometer(
  t(),
  keyword()
) :: {:ok, t()} | {:error, term()}
```

Configure the accelerometer and cache the chosen range for later scaling.

See `BMI323.Config.encode_acc_conf/1` for the supported options. `:mode` and
`:odr` are required; `:range`, `:bandwidth`, and `:averaging` have defaults.

# `configure_gyroscope`

```elixir
@spec configure_gyroscope(
  t(),
  keyword()
) :: {:ok, t()} | {:error, term()}
```

Configure the gyroscope and cache the chosen range for later scaling.

See `BMI323.Config.encode_gyr_conf/1` for the supported options.

# `default_i2c_address`

```elixir
@spec default_i2c_address() :: 104
```

The default 7-bit I²C address (`0x68`, SDO pin tied to GND).

The alternate address `0x69` is selected by tying SDO to VDDIO.

# `detect_ranges`

```elixir
@spec detect_ranges(t()) :: {:ok, t()} | {:error, term()}
```

Populate the cached ranges by reading `ACC_CONF` and `GYR_CONF`.

Useful after `acquire/1` when the device has already been configured by
some other process and you need to scale samples without reconfiguring.

# `expected_chip_id`

```elixir
@spec expected_chip_id() :: 67
```

The expected `CHIP_ID` value (`0x43`) returned by an unmodified BMI323.

# `read_accelerometer`

```elixir
@spec read_accelerometer(t()) :: {:ok, axes()} | {:error, term()}
```

Read the accelerometer x/y/z sample and return scaled values in m/s².

Requires the accelerometer range to be cached on the struct — call
`configure_accelerometer/2` or `detect_ranges/1` first.

# `read_gyroscope`

```elixir
@spec read_gyroscope(t()) :: {:ok, axes()} | {:error, term()}
```

Read the gyroscope x/y/z sample and return scaled values in rad/s.

Requires the gyroscope range to be cached on the struct — call
`configure_gyroscope/2` or `detect_ranges/1` first.

# `read_imu`

```elixir
@spec read_imu(t()) :: {:ok, imu_sample()} | {:error, term()}
```

Read accelerometer + gyroscope + temperature in a single 7-word burst.

Requires both ranges to be cached on the struct.

# `read_sensor_time`

```elixir
@spec read_sensor_time(t()) :: {:ok, non_neg_integer()} | {:error, term()}
```

Read the 32-bit sensor time counter.

One LSB equals 39.0625 µs. Multiply by
`sensor_time_tick_us/0` to get microseconds.

# `read_temperature`

```elixir
@spec read_temperature(t()) :: {:ok, float()} | {:error, term()}
```

Read the temperature sample and return °C. Returns `{:error, :invalid_sample}`
if the device reports the invalid sentinel `0x8000`.

# `sensor_time_tick_us`

```elixir
@spec sensor_time_tick_us() :: float()
```

The sensor time counter's tick period in microseconds (39.0625 µs).

# `soft_reset`

```elixir
@spec soft_reset(t()) :: {:ok, t()} | {:error, term()}
```

Issue a soft reset by writing the `0xDEAF` command to the `CMD` register.

The device returns to suspend mode and all user configuration is restored
to its power-on default. Blocks for 10 ms to satisfy
the device's post-reset start-up time before returning.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
