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

Multi-source state probe for the battery bench.

When a battery read fails, we want to know *why*: is the BEAM dead, just
unreachable, suspended in the background? The probe walks a short pipeline
of network checks and returns a typed state. The bench uses this to
produce informative trace lines and decide whether to attempt reconnection.

## State derivation

    epmd_reachable?  Node.connect  rpc_ping        →  state
    ──────────────  ─────────────  ──────────────     ─────
    false           —              —                  :unreachable
    true            false          —                  :alive_epmd_only
    true            true           timeout            :alive_dist_only
    true            true           ok                 :alive_rpc

When a `hw_udid` is provided and `ideviceinfo` is available, we additionally
probe USB battery readiness:

    ideviceinfo battery → :usb_ok | :usb_failed | :no_usb

And app-process liveness via `xcrun devicectl device info processes`:

    app_pid_alive?  →  :app_running | :app_dead | :app_unknown

All probes are independent and failure-tolerant — any single probe failing
doesn't crash the whole snapshot.

# `app_process`

```elixir
@type app_process() :: :app_running | :app_suspended | :app_dead | :app_unknown
```

Foreground/background/dead, or unknown if we can't tell.

# `reachability`

```elixir
@type reachability() ::
  :alive_rpc | :alive_dist_only | :alive_epmd_only | :unreachable
```

- `:alive_rpc` — RPC just succeeded; BEAM is fully responsive
- `:alive_dist_only` — Node.connect works, RPC times out (suspended?)
- `:alive_epmd_only` — TCP to EPMD works, dist refused (BEAM up but no dist)
- `:unreachable` — Phone offline or BEAM dead

# `screen`

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

Screen state, where derivable. `:unknown` is the honest default.

# `t`

```elixir
@type t() :: %MobDev.Bench.Probe{
  app_process: app_process(),
  battery_pct: integer() | nil,
  reachability: reachability(),
  reason: String.t() | nil,
  screen: screen(),
  ts_ms: integer(),
  usb: usb()
}
```

# `usb`

```elixir
@type usb() :: :usb_ok | :usb_failed | :no_usb
```

USB battery readiness via ideviceinfo.

# `format`

```elixir
@spec format(t()) :: String.t()
```

Format the probe result as a one-line trace fragment.

    iex> probe = %MobDev.Bench.Probe{
    ...>   ts_ms: 0, reachability: :alive_rpc, app_process: :app_running,
    ...>   usb: :no_usb, screen: :off, battery_pct: 87, reason: nil
    ...> }
    iex> MobDev.Bench.Probe.format(probe)
    "screen:off app:running rpc:ok battery:87%"

# `snapshot`

```elixir
@spec snapshot(keyword()) :: t()
```

Run the full probe and return a populated state struct.

Common options:
- `:platform` — `:ios` (default) or `:android` — selects which USB / app-
  process probes to run
- `:node` — node atom to probe (required for dist/RPC checks)
- `:host` — IP/host for EPMD probe (defaults to host portion of `:node`)
- `:rpc_timeout_ms` — defaults to 2000
- `:tcp_timeout_ms` — defaults to 1000
- `:expected_screen` — `:on | :off | :unknown` — what we *believe* the
  screen state to be (e.g. after `lock_screen`). Recorded with the snapshot.

iOS-specific:
- `:hw_udid` — hardware UDID for `ideviceinfo` USB probe
- `:device_id` — CoreDevice UUID for `devicectl` process check
- `:app_pid` — pid launched at bench start; checked against `device_id`

Android-specific:
- `:adb_serial` — ADB serial / IP:port for `adb shell` battery + process probes
- `:bundle_id` — app bundle identifier for the process-running check

---

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