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

Bluetooth Classic (BR/EDR) — device-level discovery, pairing, and
cross-profile session management.

Profile-specific operations live in submodules:

  * `Mob.Bt.Hfp` — Hands-Free Profile (audio + vendor AT commands).
    Use this for headsets, PTT-equipped earpieces, etc.
  * `Mob.Bt.Spp` — Serial Port Profile (RFCOMM byte streams).
    Use this for legacy serial-over-Bluetooth devices (Arduino HC-05,
    OBD-II readers, marine GPS, industrial sensors).
  * `Mob.Bt.Hid` — Human Interface Device (input reports).
    Use this for Bluetooth keyboards, mice, gamepads, finger PTTs.

## API style

Same as the rest of Mob: callbacks return `socket` unchanged, results
arrive in `handle_info/2` as 4-tuples:

    {:bt, event_atom, session_id_or_nil, payload}

Discovery / pairing events use `nil` for session_id; profile events
carry the session_id returned from the matching `connect/2`.

## Permissions

Bluetooth requires runtime permissions on Android 12+ (API 31+):

  * `:bluetooth_scan` — for `start_discovery/1`
  * `:bluetooth_connect` — for `pair/2`, `connect/*`, `disconnect/2`

Request via `Mob.Permissions.request/2` before calling Mob.Bt functions.

## iOS

Bluetooth Classic on iOS requires Apple's MFi (Made for iPhone)
certification — a paid, NDA-gated program. Mob.Bt is **Android-only**.
All functions return `{:error, :unsupported}` synchronously on iOS.
For iOS-equivalent custom-hardware connectivity, use `Mob.Ble`.

## Pairing flow

Two pairing modes, auto-selected by whether `:pin` is given:

    # System UI flow — Android shows a system pairing dialog
    socket = Mob.Bt.pair(socket, device)

    # Programmatic — PIN supplied via API, no UI
    socket = Mob.Bt.pair(socket, device, pin: "0000")

If the programmatic PIN fails or the device requires UI confirmation
(e.g. numeric comparison), Android falls back to the system UI
automatically.

## Disconnect

One canonical disconnect for any profile session:

    Mob.Bt.disconnect(socket, session_id)

The framework looks up which profile owns the session_id and routes
to the right profile-disconnect internally. Emits a profile-specific
event (`{:bt, :hfp_disconnected, ...}` etc).

# `device`

```elixir
@type device() :: %{
  :address =&gt; String.t(),
  :name =&gt; String.t(),
  optional(:bond_state) =&gt; :none | :bonding | :bonded,
  optional(:device_class) =&gt; non_neg_integer(),
  optional(:uuids) =&gt; [String.t()]
}
```

A discovered or paired Bluetooth device.

# `session_id`

```elixir
@type session_id() :: pos_integer()
```

An opaque session identifier for an active profile connection.

# `cancel_discovery`

```elixir
@spec cancel_discovery(socket :: term()) :: term()
```

Cancel an in-progress discovery.

# `disconnect`

```elixir
@spec disconnect(socket :: term(), session_id()) :: term()
```

Disconnect a profile session by `session_id`.

Works for any profile (`Mob.Bt.Hfp`, `Mob.Bt.Spp`, `Mob.Bt.Hid`) — the
framework dispatches internally based on which profile owns the session.

Emits a profile-specific disconnect event:

  * `{:bt, :hfp_disconnected, session_id, reason}`
  * `{:bt, :spp_disconnected, session_id, reason}`
  * `{:bt, :hid_disconnected, session_id, reason}`

# `list_paired`

```elixir
@spec list_paired(socket :: term()) :: term()
```

List currently paired (bonded) Bluetooth devices.

Result arrives as `{:bt, :paired_devices, nil, [device]}`.

# `pair`

```elixir
@spec pair(socket :: term(), device(), keyword()) :: term()
```

Pair (bond) with a Bluetooth device.

Without `:pin`, Android shows the system pairing dialog (user enters
PIN). With `:pin`, attempts programmatic pairing using the supplied
PIN; falls back to system UI if the device demands user confirmation.

Result arrives as one of:

  * `{:bt, :pair_succeeded, nil, device}`
  * `{:bt, :pair_failed, nil, %{device: device, reason: atom()}}`

# `start_discovery`

```elixir
@spec start_discovery(socket :: term()) :: term()
```

Begin Bluetooth Classic discovery. Discovered devices arrive as
individual `{:bt, :device_discovered, nil, device}` messages, terminated
by `{:bt, :discovery_finished, nil, nil}`.

Discovery typically runs ~12 seconds on Android.

# `unpair`

```elixir
@spec unpair(socket :: term(), device()) :: term()
```

Remove an existing pairing (bond).

Result: `{:bt, :unpaired, nil, device}`.

---

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