# `Mob.Motion`
[🔗](https://github.com/genericjam/mob/blob/master/lib/mob/motion.ex#L1)

Accelerometer, gyroscope, and magnetometer (compass) sensor data.

No permission required.

Updates arrive at `handle_info` at the requested interval:

    handle_info({:motion, %{
      accel:     {ax, ay, az},          # m/s² (gravity included); +g points UP at rest — see below
      gyro:      {gx, gy, gz},          # rad/s
      mag:       {mx, my, mz} | nil,    # µT (microtesla), calibrated — key present only with :magnetometer
      heading:   float | nil,           # degrees [0, 360) from MAGNETIC north — key present only with :magnetometer
      timestamp: unix_ms
    }}, socket)

## The `accel` convention

`accel` is the **specific force** (proper acceleration) in m/s², gravity
included, in the device's own axes: `+x` right, `+y` toward the top, `+z` out
of the screen. At rest it reads `+g` (≈ 9.81) on the axis pointing **away from
the ground** — held upright in portrait, `ay ≈ +9.81`; laid flat face-up,
`az ≈ +9.81`. Tilt the device and gravity redistributes across the axes; add
linear motion and it superimposes. This matches Android's `SensorManager`
`TYPE_ACCELEROMETER`, and iOS is normalized to the same sign and units (its
raw `CMMotionManager` stream is in G with the opposite gravity sign), so a
tilt- or shake-driven UI behaves identically on both platforms.

## The `:magnetometer` contract

The `mag` and `heading` keys are present **exactly when you requested
`:magnetometer`** — on both platforms. When you did, they are **always** in the
map, and each is `nil` when there's no reading yet: the device has no
magnetometer at all, or the heading hasn't been fused. So match with `nil`, and
don't assume a value is present:

    case motion do
      %{heading: deg} when is_number(deg) -> rotate_needle(deg)
      %{heading: nil} -> show_calibration_hint()   # no magnetometer, or not yet fused
    end

When you did **not** request `:magnetometer`, the map has neither key (the plain
accel/gyro stream) — so a consumer that never asked for the compass keeps
getting the exact same 3-key map, and pays no extra sensor/battery cost.

It's **magnetic** north, not true north — true north needs location + declination
(out of scope; layer it with `Mob.Location`). Magnetometers drift until
calibrated, so prompt the user to wave the phone in a figure-8, and note that
many budget devices ship without one at all (there, `heading`/`mag` stay `nil`).

iOS: `CMMotionManager` — device motion with the `XMagneticNorthZVertical` reference
frame when the magnetometer is requested and available (a calibrated field + a
fused heading on one stream); `nil`/`nil` when requested on a device without one.
Android: `SensorManager` — magnetometer + rotation-vector, registered only when
`:magnetometer` is requested.

# `sensor`

```elixir
@type sensor() :: :accelerometer | :gyro | :magnetometer
```

# `start`

```elixir
@spec start(
  Mob.Socket.t(),
  keyword()
) :: Mob.Socket.t()
```

Start sensor updates.

Options:
  - `sensors:` any subset of `[:accelerometer, :gyro, :magnetometer]`
    (default `[:accelerometer, :gyro]`). Add `:magnetometer` for the compass —
    the message then also carries `mag` + `heading`.
  - `interval_ms: integer` — update interval in milliseconds (default `100`)

# `stop`

```elixir
@spec stop(Mob.Socket.t()) :: Mob.Socket.t()
```

Stop sensor updates.

---

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