Packages
mob
0.7.17
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.26
0.6.25
0.6.24
0.6.23
0.6.22
0.6.21
0.6.20
0.6.19
0.6.18
0.6.17
0.6.16
0.6.15
0.6.14
0.6.13
0.6.12
0.6.11
0.6.10
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.2
0.6.1
0.6.0
0.5.18
0.5.17
0.5.16
0.5.15
0.5.14
0.5.11
0.5.10
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.0
0.3.10
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.0
0.1.0
BEAM-on-device mobile framework for Elixir
Current section
Files
Jump to
Current section
Files
lib/mob/motion.ex
defmodule Mob.Motion do
@moduledoc """
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)
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 `: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.
"""
@type sensor :: :accelerometer | :gyro | :magnetometer
@doc """
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`)
"""
@spec start(Mob.Socket.t(), keyword()) :: Mob.Socket.t()
def start(socket, opts \\ []) do
{sensors, interval_ms} = parse_opts(opts)
:mob_nif.motion_start(sensors, interval_ms)
socket
end
@doc false
# The pure kernel of start/2: resolves opts to the `{sensor_strings, interval_ms}`
# the NIF expects, applying defaults. Extracted (public, hidden) so the arg
# building — including that `:magnetometer` survives normalization — is
# unit-testable without a loaded NIF.
@spec parse_opts(keyword()) :: {[String.t()], pos_integer()}
def parse_opts(opts) do
sensors =
opts
|> Keyword.get(:sensors, [:accelerometer, :gyro])
|> Enum.map(&Atom.to_string/1)
{sensors, Keyword.get(opts, :interval_ms, 100)}
end
@doc """
Stop sensor updates.
"""
@spec stop(Mob.Socket.t()) :: Mob.Socket.t()
def stop(socket) do
:mob_nif.motion_stop()
socket
end
end