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 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/1option, 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-
olddefconfiggotcha), radio firmware blobs, and the app-side rootfs overlay.
The core modules carry the reference detail:
Bluez— the supervisor and every start optionBluez.Client— scanning, mode switching, adapter enumerationBluez.Gatt— the GATT request API and event contractBluez.Agent— pairing authorizationBluez.BlueAlsa— A2DP playback PCM discovery
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/1opts (funs, a PubSub, child specs); noApplication.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 GATTMTUproperty). - 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/dbusand/data/bluetooth; on a read-only rootfs point/var/lib/bluetoothat/data/bluetoothwith 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.