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

Bluetooth Classic Hands-Free Profile (HFP) — audio + vendor AT commands.

Use this for headsets, PTT-equipped earpieces (Hytera EHW02, etc), and
any device that exposes an HFP control link plus an SCO audio link.

See `Mob.Bt` for pairing, discovery, and disconnect — those are
device-level concerns. Profile-specific operations live here.

## Typical flow

    # 1. Pair (only needed once per device — Mob.Bt.pair/2)
    socket = Mob.Bt.pair(socket, device)
    # {:bt, :pair_succeeded, nil, device}

    # 2. Connect HFP profile
    socket = Mob.Bt.Hfp.connect(socket, device)
    # {:bt, :hfp_connected, session_id, device}

    # 3. (Optional) subscribe to vendor AT commands the headset emits.
    #    Hytera EHW02 fires +CTXD on PTT press, +CUTXC on release.
    socket = Mob.Bt.Hfp.subscribe_vendor_at(socket, session_id)
    # {:bt, :vendor_at, session_id, %{cmd: "+CTXD", args: ""}}

    # 4. (Optional) bring up the SCO audio link.
    socket = Mob.Bt.Hfp.start_sco(socket, session_id)
    # {:bt, :sco_started, session_id, %{sample_rate: 8000, ...}}
    # then audio chunks stream as:
    # {:bt, :sco_audio_in, session_id, pcm_bytes}

    # 5. Send PCM audio out to the headset earpiece:
    Mob.Bt.Hfp.send_audio(socket, session_id, pcm_bytes)

    # 6. Disconnect (one canonical path — Mob.Bt.disconnect/2)
    Mob.Bt.disconnect(socket, session_id)

## Vendor AT commands

HFP defines a small core AT vocabulary (call control, volume, ring).
Headset vendors extend with their own `+`-prefixed commands. Subscribing
via `subscribe_vendor_at/2` delivers any *unrecognized* AT command from
the headset as `{:bt, :vendor_at, session_id, %{cmd, args}}` for your
app to interpret.

Sending a vendor AT command to the headset is `send_vendor_at/4`.
Standard responses (`OK`, `ERROR`) are emitted by Android automatically —
use the `response` argument to override only when the AT spec demands
custom payload.

## SCO audio

SCO (Synchronous Connection-Oriented) is the real-time bidirectional
voice channel HFP uses for call audio. `start_sco/2` opens it; PCM
bytes flow both ways until `stop_sco/2` or disconnect.

Format is 8 kHz / 16-bit / mono PCM by default; modern devices may
negotiate up to 16 kHz wideband (mSBC). The `:sco_started` event
reports the negotiated parameters.

# `connect`

```elixir
@spec connect(socket :: term(), Mob.Bt.device()) :: term()
```

Open an HFP profile connection to `device`. The device must already
be paired (`Mob.Bt.pair/2`).

Result: `{:bt, :hfp_connected, session_id, device}` on success,
`{:bt, :hfp_connect_failed, nil, %{device: device, reason: atom()}}`
on failure.

# `send_audio`

```elixir
@spec send_audio(socket :: term(), Mob.Bt.session_id(), binary()) :: term()
```

Send PCM audio bytes out the SCO link to the headset earpiece.

Bytes are linear PCM matching the format reported in `:sco_started`
(typically 8 kHz / 16-bit / mono signed little-endian).

Returns the socket. This is fire-and-forget; no completion event.

# `send_vendor_at`

```elixir
@spec send_vendor_at(socket :: term(), Mob.Bt.session_id(), String.t(), String.t()) ::
  term()
```

Send a vendor AT command to the headset. Useful for headset-specific
feature toggles or query/response protocols.

    Mob.Bt.Hfp.send_vendor_at(socket, session, "+XAPL", "0505,2")

# `start_sco`

```elixir
@spec start_sco(socket :: term(), Mob.Bt.session_id()) :: term()
```

Open the SCO audio link for this HFP session.

Emits `{:bt, :sco_started, session_id, %{sample_rate: integer, encoding: atom, channels: integer}}`
when the link is up. Mic audio then streams as
`{:bt, :sco_audio_in, session_id, pcm_bytes}`.

On failure: `{:bt, :sco_failed, session_id, reason}`.

# `stop_sco`

```elixir
@spec stop_sco(socket :: term(), Mob.Bt.session_id()) :: term()
```

Close the SCO audio link without disconnecting the HFP session.

Emits `{:bt, :sco_stopped, session_id, nil}`.

# `subscribe_vendor_at`

```elixir
@spec subscribe_vendor_at(socket :: term(), Mob.Bt.session_id(), keyword()) :: term()
```

Subscribe to vendor-specific AT commands emitted by the headset on the
given HFP session.

The caller specifies which BT SIG company IDs to listen for via the
`:company_ids` option. Android's ACTION_VENDOR_SPECIFIC_HEADSET_EVENT
broadcasts are only delivered for explicitly-registered IDs, so a
default empty list means no events will be received.

Common values:

  * `313`  — Hytera (PTT commercial radios)
  * `76`   — Apple (AirPods custom events)
  * `10`   — Qualcomm
  * `1117` — Plantronics / Poly

Standard (non-vendor) AT commands are handled by Android's HFP stack
and never surface here.

Stream events: `{:bt, :vendor_at, session_id, %{cmd: String.t(), cmd_type: integer(), args: String.t(), address: String.t()}}`.

## Example

    Mob.Bt.Hfp.subscribe_vendor_at(socket, session_id, company_ids: [313])

---

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