# `Bluez.Gatt`
[🔗](https://github.com/bbangert/bluez/blob/v0.1.0/lib/bluez/gatt.ex#L1)

Active BLE connections + GATT client over BlueZ D-Bus — the engine an
ESPHome-proxy-style host puts behind its Bluetooth-proxy adapter.

Owns its own `rebus` connection, separate from `Bluez.Client`
(the passive scanner): independent match rules, independent failure domain,
and zero changes to the hardware-verified advert path. Concurrent method
calls on one rebus connection don't serialize (replies are correlated by
serial), but every call still blocks its *calling* process — so all BlueZ
calls here run in `Task`s under `Bluez.Gatt.Tasks`, never in
this GenServer's own loop. `Device1.Connect` alone can take ~25 s.

## Event contract

Results flow asynchronously to the `subscriber` pid captured at
`connect/3`, through the `on_gatt_event:` fun (`fn subscriber, event`;
default: `send(subscriber, event)`). Hosts inject a translator to reshape
events for their own wire protocol. The full event set:

  * `{:gatt_connection, address, {:ok, mtu} | {:error, code}}` — connect
    result, post-remove teardown, or an unexpected disconnect.
  * `{:gatt_service, address, %Bluez.Gatt.Service{}}` — one
    per service, streamed on `get_services/1`.
  * `{:gatt_services_done, address}` — service stream terminator.
  * `{:gatt_read, address, handle, {:ok, binary} | {:error, code}}` —
    characteristic *and* descriptor reads (a failed `get_services/1` on a
    not-ready link also answers here with handle 0, the ESPHome
    convention).
  * `{:gatt_write, address, handle, {:ok, :done} | {:error, code}}` —
    characteristic and descriptor writes.
  * `{:gatt_notify, address, handle, {:ok, :done} | {:error, code}}` —
    Start/StopNotify result.
  * `{:gatt_notify_data, address, handle, binary}` — a notification value.
  * `{:gatt_pair, address, success? :: boolean(), code :: integer()}`
  * `{:gatt_unpair, address, success?, code}`
  * `{:gatt_clear_cache, address, success?, code}`

Error `code`s follow the ESPHome BLE convention this stack was built
against: `-1` generic, `-2` not connected.

## Options

  * `on_gatt_event:` — see above.
  * `on_connections_changed:` — zero-arity fun invoked whenever a
    connection slot is taken or freed (default: no-op) — e.g. a stats
    ticker.

## Connection lifecycle

```mermaid
flowchart LR
    connect["connect cast"] --> dev1["Device1.Connect (Task)"]
    dev1 --> resolved{"ServicesResolved?"}
    resolved -- true --> gmo["GetManagedObjects"]
    resolved -- false --> wait["wait for signal<br/>(resolve timeout)"]
    wait --> gmo
    gmo --> tree["GattTree.build"]
    tree --> host(["{:gatt_connection, addr, {:ok, mtu}} → host"])
```

The connection reply is deliberately deferred until BlueZ has resolved
services: every subsequent GATT request is handle-keyed, and the
handle ↔ object-path map only exists once the GATT objects are visible.
MTU comes from the experimental `MTU` characteristic property
(`bluetoothd -E`), falling back to the BLE minimum (23).

Unexpected disconnects surface as `Device1.PropertiesChanged
Connected=false`; the subscriber is told via the same connection envelope
with an error code. Requested disconnects remove state immediately and
need no follow-up message.

## Notifications

`StartNotify` makes BlueZ emit `PropertiesChanged` with `Value` on the
characteristic path — the same signal mechanism as adverts. The
char-path → `{address, handle}` route is registered *before* the
StartNotify call returns (and rolled back on error) so no early value
can race past us.

The host owns cross-client address locking; this
module trusts that `connect` arrives at most once per address per
ownership cycle, but stays defensive (a stale entry is torn down and
replaced).

## Pairing and cache clearing (Phase 2)

`pair/1` calls `Device1.Pair()` — IO is negotiated through
`Bluez.Agent` (the default NoInputNoOutput agent), and the
Pair Task brackets the call with `expect_pairing/1`/`pairing_done/1` so
the agent only authorizes pairings we initiated. `unpair/1` and
`clear_cache/1` both map to `Adapter1.RemoveDevice` — BlueZ's only
bond-removal API, and the only D-Bus way to drop a device's cached GATT
database (they differ only in the reply envelope; the bond, if any,
goes too — same observable semantics as ESP32's
`esp_ble_remove_bond_device`). RemoveDevice destroys the device object —
and the live link with it.

Hardware-observed ordering hazard: BlueZ disconnects the device (and
emits `Connected=false`) *while processing* RemoveDevice, before the
method returns — and a failed `Pair()` can likewise drop the link before
its error reply lands. Either way the signal path tears the entry down
first, so pair/remove Task messages carry the subscriber pid themselves:
the op reply is always delivered, and entry teardown happens via
whichever of the two paths (signal or result) still finds the entry.

All three require a live connection entry: replies route to the
subscriber captured at `connect/3`, so for an unknown address there
is no one to answer — those requests are logged and dropped (HA only
issues them on connected devices).

# `address`

```elixir
@type address() :: non_neg_integer()
```

Packed 48-bit MAC, MSB-first.

# `child_spec`

Returns a specification to start this module under a supervisor.

See `Supervisor`.

# `clear_cache`

```elixir
@spec clear_cache(address()) :: :ok
```

Drop BlueZ's cached GATT database for the device. Same underlying
operation as `unpair/1` (`RemoveDevice` is the only D-Bus API for it —
the bond, if any, goes too; same observable semantics as ESP32's
`esp_ble_remove_bond_device`), differing only in the reply envelope:
`{:gatt_clear_cache, address, success?, code}`.

# `connect`

```elixir
@spec connect(address(), keyword(), pid()) :: :ok
```

Open a BLE connection to `address` and capture `subscriber` as the pid
all of this connection's events route to (via the `on_gatt_event:` fun).

Cast-style: returns `:ok` immediately. The outcome arrives as
`{:gatt_connection, address, {:ok, mtu} | {:error, code}}` — deferred
until BlueZ has resolved services, so every handle-keyed request below
is valid the moment the success event lands. `opts` are accepted for
host-side compatibility and currently unused.

Refused (with an error event) when `address` is not a 48-bit MAC or all
`max_connections/0` slots are taken.

# `connections_free`

```elixir
@spec connections_free() :: {non_neg_integer(), non_neg_integer()}
```

Free / total connection slots (the host's connections_free callback).

# `disconnect`

```elixir
@spec disconnect(address()) :: :ok
```

Tear down `address`'s connection. Requested disconnects emit no
follow-up event (the entry is dropped before BlueZ reports the link
down); unknown addresses are a no-op.

# `get_services`

```elixir
@spec get_services(address()) :: :ok
```

Stream the connected device's GATT database to the subscriber: one
`{:gatt_service, address, %Bluez.Gatt.Service{}}` per service, then
`{:gatt_services_done, address}`. On a not-ready link the failure is
reported as `{:gatt_read, address, 0, {:error, -2}}` (the ESPHome
convention for a failed service listing).

# `max_connections`

```elixir
@spec max_connections() :: pos_integer()
```

Total active-connection slots this GATT client offers.

# `notify`

```elixir
@spec notify(address(), non_neg_integer(), boolean()) :: :ok
```

Start (`enable?: true`) or stop notifications/indications on the
characteristic at `handle`. The call result arrives as
`{:gatt_notify, address, handle, {:ok, :done} | {:error, code}}`;
subsequent values arrive as `{:gatt_notify_data, address, handle,
binary}`. The value route is registered before StartNotify returns so
no early notification can be lost.

# `pair`

```elixir
@spec pair(address()) :: :ok
```

Bond with the connected device (`Device1.Pair()`), with IO negotiated
through `Bluez.Agent` — only pairings initiated here are authorized.
Result: `{:gatt_pair, address, success? :: boolean(), code}`. A failed
pairing can drop the link (hardware-observed), in which case a
`{:gatt_connection, address, {:error, _}}` teardown event follows.

# `read`

```elixir
@spec read(address(), non_neg_integer()) :: :ok
```

Read the characteristic at `handle` (a *value* handle, as reported in
the service stream). Result: `{:gatt_read, address, handle, {:ok,
binary} | {:error, code}}`. Falls back to a descriptor read when the
handle names a descriptor.

# `read_descriptor`

```elixir
@spec read_descriptor(address(), non_neg_integer()) :: :ok
```

Read the descriptor at `handle`. Same result envelope as `read/2`
(`{:gatt_read, ...}`); falls back to a characteristic read when the
handle names one.

# `start_link`

# `task_supervisor`

```elixir
@spec task_supervisor() :: module()
```

Name of the Task.Supervisor all BlueZ calls run under.

# `unpair`

```elixir
@spec unpair(address()) :: :ok
```

Remove the device's bond via `Adapter1.RemoveDevice` — BlueZ's only
bond-removal API, which also destroys the device object and any live
link. Result: `{:gatt_unpair, address, success?, code}`, followed by a
`{:gatt_connection, address, {:error, -2}}` teardown event when a
connection was up.

# `write`

```elixir
@spec write(address(), non_neg_integer(), binary(), boolean()) :: :ok
```

Write `data` to the characteristic at `handle`. `response?` selects
Write-With-Response (`true`) vs Write-Without-Response. Result:
`{:gatt_write, address, handle, {:ok, :done} | {:error, code}}`.
Writes larger than the 512-byte ATT attribute limit are refused
up front.

# `write_descriptor`

```elixir
@spec write_descriptor(address(), non_neg_integer(), binary()) :: :ok
```

Write `data` to the descriptor at `handle` (always Write-With-Response).
Same result envelope as `write/4` (`{:gatt_write, ...}`).

---

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