<!--
SPDX-FileCopyrightText: 2026 James Harton

SPDX-License-Identifier: Apache-2.0
-->

# bb_sensor_bmi323

[Beam Bots](https://github.com/beam-bots/bb) integration for the Bosch
[BMI323](https://www.bosch-sensortec.com/products/motion-sensors/imus/bmi323/)
6-DoF inertial measurement unit (accelerometer + gyroscope) over I2C.

Operates the chip in either *polling* mode (periodic register reads) or
*interrupt* mode (FIFO watermark on INT1) and publishes
`BB.Message.Sensor.Imu` messages with angular velocity (rad/s) and linear
acceleration (m/s²).

The BMI323 has no magnetometer, so `orientation` is published as the
identity quaternion. **You almost always want to pair this sensor with an
orientation estimator** such as
[`bb_estimator_ahrs`](https://hex.pm/packages/bb_estimator_ahrs).

## Choosing a mode

| ODR ≤ 200 Hz                          | ODR > 200 Hz                          |
| ------------------------------------- | ------------------------------------- |
| `mode: :polling` — simple, no GPIO    | `mode: :interrupt` — needs INT1 wired |
| Low jitter, low overhead              | Reliable up to the chip's 6.4 kHz ODR |
| No FIFO, samples read one at a time   | FIFO-buffered, samples arrive in bursts of `watermark_frames` |

In interrupt mode, a `watermark_frames: 8` setting at ODR 800 Hz means
batches every 10 ms. Downstream consumers (AHRS estimators, kinematics
filters) typically cope with bursts naturally since each sample's `dt` is
read from its own monotonic timestamp.

## Usage

### Polling mode

```elixir
defmodule MyRobot do
  use BB

  topology do
    link :base do
      sensor :imu, {BB.Sensor.BMI323,
        bus: "i2c-1",
        address: 0x68,
        mode: :polling,
        accelerometer_range: 8,
        accelerometer_odr: 200,
        gyroscope_range: 2000,
        gyroscope_odr: 200,
        publish_rate: ~u(100 hertz)
      }
    end
  end
end
```

### Interrupt mode

Wire the chip's INT1 pin to a GPIO and let the on-chip FIFO buffer
samples:

```elixir
topology do
  link :base do
    sensor :imu, {BB.Sensor.BMI323,
      bus: "i2c-1",
      address: 0x68,
      mode: :interrupt,
      int1_pin: 17,
      accelerometer_range: 8,
      accelerometer_odr: 800,
      gyroscope_range: 2000,
      gyroscope_odr: 800,
      watermark_frames: 8
    }
  end
end
```

### Pairing with an AHRS estimator

The whole point of the identity `orientation` field is that an estimator
fills it in:

```elixir
topology do
  link :base do
    sensor :imu, {BB.Sensor.BMI323, bus: "i2c-1", ...} do
      estimator :orientation, {BB.Estimator.Ahrs.Madgwick, beta: 0.1}
    end
  end
end
```

The estimator subscribes to the sensor's `Imu` messages, replaces the
identity quaternion with a fused orientation, and republishes. See
`bb_estimator_ahrs` for the three available algorithms (Madgwick, Mahony,
Complementary) and their tuning options.

Subscribe to the final stream:

```elixir
BB.subscribe(MyRobot, [:sensor, :base, :imu])
```

## Coordinate frame

Axes are the chip's own +X / +Y / +Z silkscreen (see the BMI323 datasheet
§3.2). The BB topology entity this sensor attaches to *is* its coordinate
frame — orient the IMU on the link as appropriate and apply a static
transform downstream if the chip mounting doesn't match link axes.

## Options

| Option                 | Default       | Description                                                |
| ---------------------- | ------------- | ---------------------------------------------------------- |
| `bus`                  | _required_    | I2C bus name (e.g. `"i2c-1"`)                              |
| `address`              | `0x68`        | I2C address (`0x68` SDO→GND, `0x69` SDO→VDDIO)             |
| `mode`                 | `:polling`    | `:polling` or `:interrupt`                                 |
| `accelerometer_range`  | `8`           | g (`2`, `4`, `8`, `16`)                                    |
| `accelerometer_odr`    | `200`         | Hz (`12.5`..`6400`)                                        |
| `accelerometer_mode`   | `:normal`     | `:normal`, `:low_power`, `:high_performance`, `:disabled`  |
| `gyroscope_range`      | `2000`        | °/s (`125`, `250`, `500`, `1000`, `2000`)                  |
| `gyroscope_odr`        | `200`         | Hz                                                         |
| `gyroscope_mode`       | `:normal`     | as for accelerometer                                       |
| `publish_rate`         | `~u(100 hertz)` | Polling rate (polling mode only)                         |
| `int1_pin`             | _required if `mode: :interrupt`_ | GPIO pin number wired to BMI323 INT1   |
| `watermark_frames`     | `8`           | FIFO frames per interrupt (interrupt mode only)            |

Setting `accelerometer_mode` or `gyroscope_mode` to `:disabled` powers
that axis down — the IMU will publish constant / invalid values for it
until the mode is changed back.

## Runtime parameter changes

Live (no restart):

- `publish_rate` (polling mode) — interval is recomputed.
- `accelerometer_*` / `gyroscope_*` — chip is reconfigured.

Triggers `{:stop, :reconfigure}` (supervisor restarts with new params):

- `mode`, `bus`, `address`, `int1_pin`, `watermark_frames`.

## Troubleshooting

- **`{:chip_id_mismatch, got: id, expected: 0x43}`** — the device at the
  configured I2C address isn't a BMI323. Check `address` (`0x68` SDO→GND,
  `0x69` SDO→VDDIO), the bus, and that the chip is powered.
- **`:no_such_bus`** — the `bus` string doesn't match any `/dev/i2c-*`
  device. On Linux: `i2cdetect -l`.
- **`:int1_pin_required_for_interrupt_mode`** — `mode: :interrupt` was
  set without an `int1_pin`.
- **GPIO acquire failure** in interrupt mode — pin may already be
  exported, owned by another process, or not exist on this board.
- **Constant / silently-wrong samples after changing `*_mode`** — make
  sure you didn't leave an axis on `:disabled`.

See `BB.Sensor.BMI323` for full module documentation.
