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

Build a GATT tree from BlueZ's `GetManagedObjects` reply.

Once a device's `ServicesResolved` flips true, its GATT database appears
as `org.bluez.GattService1` / `GattCharacteristic1` / `GattDescriptor1`
objects under the device path. This module turns those (rebus-decoded)
objects into:

  * `Bluez.Gatt.Service` structs, emitted to the host via
    `{:gatt_service, address, service}` events (the app-side translator
    reshapes them for Home Assistant), and
  * handle ↔ object-path maps for executing handle-keyed GATT requests.

Pure and host-testable — the D-Bus I/O stays in `Bluez.Gatt`.

## Handle convention (bleak-compatible)

BlueZ encodes the ATT handle of each attribute in the object path's hex
suffix (`service000a/char000b/desc000d`). For characteristics that is the
*declaration* handle; clients address the *value* attribute, which always
sits at declaration + 1. We report `path_handle + 1` for characteristics
(exactly what bleak — HA's own BlueZ backend — does), and the path handle
as-is for services and descriptors. HA echoes our reported handles back
in GATT requests, and `by_handle` is keyed by the same reported handles,
so the mapping is self-consistent — and consistent with HA's cached GATT
databases from bleak/ESP32 proxies, which also carry value handles.

## Hierarchy

Children are attached via their parent object-path properties
(`Characteristic.Service`, `Descriptor.Characteristic`) rather than path
string prefixes. Objects with malformed paths (no parseable handle) or
dangling parent references are dropped.

# `kind`

```elixir
@type kind() :: :characteristic | :descriptor
```

# `t`

```elixir
@type t() :: %Bluez.GattTree{
  by_handle: %{required(non_neg_integer()) =&gt; {kind(), String.t()}},
  handle_by_char_path: %{required(String.t()) =&gt; non_neg_integer()},
  mtu: non_neg_integer() | nil,
  services: [Bluez.Gatt.Service.t()]
}
```

# `build`

```elixir
@spec build([{String.t(), list()}], String.t()) :: t()
```

Build the GATT tree for `device_path` from a `GetManagedObjects` object
list (rebus-decoded: `[{path, [{iface, props_list}]}]`).

`mtu` is the ATT MTU reported by any of the device's characteristics
(`bluetoothd -E` exposes the experimental `MTU` property), or `nil` when
none carries one.

# `properties_mask`

```elixir
@spec properties_mask([String.t()] | term()) :: non_neg_integer()
```

Map a BlueZ `Flags` string list to the GATT properties bitmask.

    iex> Bluez.GattTree.properties_mask(["read", "notify"]) == 0x12
    true

# `to_uuid`

```elixir
@spec to_uuid(String.t() | term()) :: Bluez.Gatt.Service.uuid()
```

Convert a BlueZ UUID string to the `t:Bluez.Gatt.Service.uuid/0`
shape: a 16-bit integer when it's a SIG base UUID (smaller on the wire,
matches what ESP32 proxies send), else the full 16-byte binary. Invalid
strings map to the all-zero UUID rather than crashing the tree build.

---

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