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 or any BLE-consuming Elixir application needs on embedded Linux.

Built for Nerves devices (read-only rootfs, 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, 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 — 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 — 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 — 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:

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) 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.
  • 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

def deps do
  [
    {:bluez, "~> 0.1"}
  ]
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 as Bluez.Rebus.* (MIT, with the service-side additions proposed upstream in ausimian/rebus#9 — see lib/bluez/rebus/VENDORED.md). The namespacing means this can never collide with a rebus dependency of your own.

Usage

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)
   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:

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 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.
  • 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 for the full customization list (buildroot packages, kernel fragment, firmware blobs, overlay).

License

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