# `MobDev.Bench.DeviceObserver`
[🔗](https://github.com/genericjam/mob_dev/blob/master/lib/mob_dev/bench/device_observer.ex#L1)

Subscribes to `Mob.Device` events on the running app over Erlang
distribution and tracks ground-truth screen/app state for the bench.

Without this, the bench only knows what *it* asked the device to do
("we just ran lock_screen, so the screen *should* be off"). With this,
the bench learns from the device what's actually happening
(`{:mob_device, :did_enter_background}`, `{:mob_device, :screen_off}`),
and the probe snapshots reflect reality.

## Lifecycle

    observer = DeviceObserver.subscribe(node, categories: [:app, :display])
    ...
    observer = DeviceObserver.consume_messages(observer)  # call each tick
    observer.screen   # => :on | :off | :unknown
    observer.app      # => :running | :background | :suspended | :unknown
    observer.events   # => list of recent events (most recent first)

Subscription is best-effort — if the device's BEAM doesn't have
`Mob.Device.subscribe/1` exported (older app build), `subscribe/2`
returns an observer that just passes through the caller's expected
state.

# `app_state`

```elixir
@type app_state() :: :running | :background | :suspended | :unknown
```

# `screen_state`

```elixir
@type screen_state() :: :on | :off | :unknown
```

# `t`

```elixir
@type t() :: %MobDev.Bench.DeviceObserver{
  app: app_state(),
  events: [{integer(), atom(), term()}],
  last_event_ts_ms: integer() | nil,
  node: atom() | nil,
  screen: screen_state(),
  subscribed?: boolean()
}
```

# `apply_to_probe`

```elixir
@spec apply_to_probe(t(), MobDev.Bench.Probe.t()) :: MobDev.Bench.Probe.t()
```

Merge the observer's ground-truth state into a Probe snapshot. If the
observer has authoritative state, prefer it over what the probe inferred;
fall back to the probe's view otherwise.

# `consume_messages`

```elixir
@spec consume_messages(t()) :: t()
```

Drain the calling process's mailbox of pending Mob.Device messages and
update the observer's tracked state. Returns the updated observer.

Call this at the top of each poll cycle. Non-blocking — uses `receive`
with `after 0`.

# `subscribe`

```elixir
@spec subscribe(
  atom() | nil,
  keyword()
) :: t()
```

Try to subscribe the calling process to `Mob.Device` events on `node`.
Returns an observer struct, possibly with `subscribed?: false` if the
device's app doesn't support it (older build).

---

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