Packages

BlueZ-over-D-Bus client for Elixir: BLE scanning, GATT, pairing, and bluez-alsa audio

Current section

Files

Jump to
bluez README.md
Raw

README.md

# Bluez

BlueZ-over-D-Bus client library for Elixir: BLE scanning (passive *and*
active), active GATT connections, pairing, and bluez-alsa A2DP audio — the
pieces an [ESPHome-style Bluetooth
proxy](https://esphome.io/components/bluetooth_proxy.html) or any
BLE-consuming Elixir application needs on embedded Linux.

Built for [Nerves](https://nerves-project.org/) devices (read-only rootfs,
[`MuonTrap`](https://hex.pm/packages/muontrap)-supervised daemons) but has no
Nerves dependency: it runs on any Linux host where your application may own
the system D-Bus instance.

## Status

Early extraction from
[`universal_proxy`](https://github.com/bbangert/universal_proxy), where this
code shipped and was hardware-validated as an ESPHome Bluetooth proxy on
Raspberry Pis (passive/active scanning into Home Assistant, GATT
connect/read/write/notify/pair against real peripherals, BT-headphone
audio). Still being iterated on; not a final API.

## Documentation

Start here once you're ready to go beyond the quickstart below:

- [Architecture guide](guides/architecture.md) — the supervision tree and
  its restart semantics, the two rebus connections, scan-mode transitions,
  advert reconstruction, the GATT connection lifecycle, adapter selection,
  and the benign failure loops.
- [Host integration guide](guides/host_integration.md) — the integration
  cookbook: every `Bluez.start_link/1` option, the full GATT event
  contract with a translator example, audio PCM discovery, and the
  runtime radio-switching pattern.
- [Nerves system guide](guides/nerves_system.md) — the customized Nerves
  system this library requires: buildroot packages, the kernel fragment
  (and its silent-`olddefconfig` gotcha), radio firmware blobs, and the
  app-side rootfs overlay.

The core modules carry the reference detail:

- `Bluez` — the supervisor and every start option
- `Bluez.Client` — scanning, mode switching, adapter enumeration
- `Bluez.Gatt` — the GATT request API and event contract
- `Bluez.Agent` — pairing authorization
- `Bluez.BlueAlsa` — A2DP playback PCM discovery
- `Bluez.Mgmt` — LE advertising with verbatim AD bytes (kernel MGMT)
- `Bluez.Radio` — per-controller ownership for advertising
- `Bluez.AdvertReceiver` — verbatim advertising reports off the HCI socket
- `Bluez.HciUser` — exclusive raw HCI access to a dedicated controller

## Features

- **One supervisor** that brings up `dbus-daemon`, `bluetoothd -E`, and
  (optionally) `bluealsad`, with crash-isolation and restart ordering
  (`:rest_for_one`) already worked out — an audio fault never restarts
  scanning, a bus restart rebuilds everything above it.
- **Passive and active BLE scanning**, runtime-switchable. Passive mode
  uses BlueZ's `AdvertisementMonitor1` (no scan requests — peripherals
  don't burn battery answering); active mode collects SCAN_RSP data.
- **Advertisement reconstruction** — BlueZ only exposes parsed properties;
  they're re-serialized into AD byte structures, emit-gated (first
  sighting / payload change / RSSI heartbeat, configurable via
  `Bluez.Client`'s `rssi_heartbeat_ms:` option with default 10 s) and
  LRU-capped, then fanned out through your `on_advertisement:` fun.
- **Handle-keyed GATT client** — connect / service discovery / read /
  write / notify / pair / unpair / clear-cache, with a documented,
  host-agnostic event contract delivered through your `on_gatt_event:`
  fun. Generation-stamped so late replies can't corrupt replaced
  connections.
- **Pairing agent** that authorizes exactly the pairings your code
  initiated.
- **LE advertising** (opt-in) — `Bluez.Mgmt` puts verbatim AD bytes on
  air through the kernel MGMT socket, including layouts BlueZ's D-Bus API
  refuses (Flags, names, 128-bit UUID lists, ordered 16-bit UUID lists
  with duplicates), with a settable interval and millisecond-precise
  durations, on any controller the kernel drives. It sends under a
  `Bluez.Radio` lease: one owner per controller, and everything it sent
  goes off air when the lease ends.
- **Verbatim advert receive** (opt-in) — `Bluez.AdvertReceiver` reads the
  controller's legacy and extended advertising reports off an HCI raw
  socket, AD bytes untouched, for protocols that BlueZ's per-device
  property merging would destroy. Pure `:socket`, no NIF or port.
- **Exclusive controller access** (opt-in) — `Bluez.HciUser` takes a
  dedicated controller away from `bluetoothd` over an `HCI_CHANNEL_USER`
  socket and speaks raw HCI: commands with the controller's flow control
  and timeouts, advertising reports and errors delivered to an owner
  process, and the controller handed back to BlueZ on close. For protocols
  that scan and advertise continuously, where `bluetoothd` would cost more
  CPU than the protocol itself.
- **bluez-alsa integration** (optional) — enumerates ready-to-open A2DP
  playback PCMs and signals your app when the set changes.
- **No host coupling** — configuration flows exclusively through
  `start_link/1` opts (funs, a PubSub, child specs); no
  `Application.get_env`, no callbacks into named host modules.

## Installation

```elixir
def deps do
  [
    {:bluez, "~> 0.6"}
  ]
end
```

A note on the D-Bus layer: exporting objects (which BlueZ's passive
scanning requires) needs a service-side API that isn't in any released
D-Bus client, so this library ships a vendored, **namespaced** copy of
[rebus](https://github.com/ausimian/rebus) as `Bluez.Rebus.*` (MIT, with
the service-side additions proposed upstream in
[ausimian/rebus#9](https://github.com/ausimian/rebus/pull/9) — see
`lib/bluez/rebus/VENDORED.md`). The namespacing means this can never
collide with a rebus dependency of your own.

## Usage

```elixir
children = [
  {Bluez,
   client: [on_advertisement: &MyApp.Scanner.on_advertisement/1],
   gatt: [on_gatt_event: &MyApp.BLEProxy.gatt_event/2],
   desired_adapter: nil,     # nil = auto (lowest-index adapter)
   exclude_adapters: [],     # MACs of radios this stack must never drive
   audio: true}              # bluealsad + Bluez.BlueAlsa
]
```

Adverts then arrive at your scanner fun as maps
(`%{address:, rss:, address_type:, raw_data:}`), and GATT requests are
cast-style with results delivered as events:

```elixir
Bluez.Gatt.connect(0xAABBCCDDEEFF, [], self())
receive do
  {:gatt_connection, addr, {:ok, mtu}} -> Bluez.Gatt.get_services(addr)
  {:gatt_connection, _addr, {:error, code}} -> {:error, code}
end
```

See the [host integration guide](guides/host_integration.md) for the full
option surface and event contract.

## Runtime requirements

- BlueZ ≥ 5.66, started by this supervisor with `-E` (experimental — needed
  for passive scanning and the GATT `MTU` property).
- dbus (this supervisor owns the *system* bus — don't run it next to a
  distro dbus/bluetoothd).
- Optional: bluez-alsa v4 for `audio: true`.
- Optional: `CAP_NET_RAW`/`CAP_NET_ADMIN` (root on Nerves) for
  `Bluez.AdvertReceiver`/`Bluez.Mgmt`/`Bluez.HciUser`; Linux ≥ 5.17 for `Bluez.Mgmt`
  (extended advertising commands, queued by the kernel instead of refused
  as busy while another is pending).
- Writable `/run/dbus` and `/data/bluetooth`; on a read-only rootfs point
  `/var/lib/bluetooth` at `/data/bluetooth` with an overlay symlink.

On Nerves none of this is in the official systems — see the
[Nerves system guide](guides/nerves_system.md) for the full customization
list (buildroot packages, kernel fragment, firmware blobs, overlay).

## License

Apache-2.0 — see the LICENSE file in the repository.