Skip to main content

guides/nerves_system.md

# Nerves system requirements

The official Nerves systems ship **neither BlueZ nor dbus**, so this
library needs a customized system. This guide lists exactly what that
system must add — every item below was hardware-found bringing the stack
up on Raspberry Pis (a known-good reference implementation for eight
targets lives at
[`bbangert/nerves_systems_universal_proxy`](https://github.com/bbangert/nerves_systems_universal_proxy)).

## Buildroot packages (`nerves_defconfig`)

```text
BR2_PACKAGE_DBUS=y
BR2_PACKAGE_BLUEZ5_UTILS=y
```

`BLUEZ5_UTILS` installs `bluetoothd` at
`/usr/libexec/bluetooth/bluetoothd` — the library's default
`bluetoothd_path:`. BlueZ must be ≥ 5.66 for the
`AdvertisementMonitorManager1` passive-scanning API (the library always
starts the daemon with `-E`).

For `audio: true`, additionally:

```text
BR2_PACKAGE_BLUEZ_ALSA=y
```

bluez-alsa's PCM is a **userspace ALSA plugin** — no kernel audio config
is needed (unlike, say, `snd-usb-audio`). Buildroot ships bluez-alsa 4.x,
whose daemon is named `bluealsad`; the library probes both the v4 and v3
binary names. The stock package builds SBC only — fine for A2DP; codec
extras (AAC etc.) have licensing implications and are your call.

### Radio firmware blobs

- **Raspberry Pi onboard radios**: `BR2_PACKAGE_RPI_DISTRO_BLUEZ_FIRMWARE=y`
  (a `nerves_system_br` package) installs the Pi `.hcd` set with the
  per-board symlinks (e.g.
  `BCM4345C0.raspberrypi,3-model-b-plus.hcd → BCM4345C0.hcd`), so the
  kernel's `btbcm` picks the right patchram file from the device tree
  compatible string. (Trivia that bites raw-HCI stacks: LMP subversion
  `0x6119` is BCM4345**C0** — Pi 3 B+/3 A+ — while `0x6606` is C5, Pi
  400/CM4. The kernel path gets this right by itself.)
- **Realtek USB dongles** (RTL8761B/BU — most cheap "BT 5.0" dongles):
  `BR2_PACKAGE_LINUX_FIRMWARE=y` + `BR2_PACKAGE_LINUX_FIRMWARE_RTL_87XX_BT=y`
  for the `rtl_bt/` blobs.

## Kernel fragment

For a UART-attached radio (Pi onboard chips) the kernel must auto-attach
`hci0` via serdev:

```text
CONFIG_BT=y
CONFIG_BT_BREDR=y
CONFIG_BT_LE=y
CONFIG_BT_HCIUART=y
CONFIG_BT_HCIUART_SERDEV=y
CONFIG_BT_HCIUART_BCM=y
# THE GOTCHA — see below:
CONFIG_SERIAL_DEV_BUS=y
CONFIG_SERIAL_DEV_CTRL_TTYPORT=y
```

**The gotcha**: `CONFIG_BT_HCIUART_SERDEV` only *depends on*
`SERIAL_DEV_BUS` — it does **not** `select` it. If `SERIAL_DEV_BUS` is in
neither your fragment nor the base defconfig, `make olddefconfig`
**silently drops `BT_HCIUART_SERDEV` back to `n`**: the firmware boots,
`bluetoothd` runs, the `.hcd` files are present, the device tree is
correct — and `/sys/class/bluetooth/` stays empty with zero BT lines in
dmesg. When writing kernel fragments, list every `depends on`
prerequisite explicitly; `olddefconfig` only auto-resolves `select`ed
symbols.

For USB dongles (also the only path on boards without an onboard radio):

```text
CONFIG_BT_HCIBTUSB=y
CONFIG_BT_HCIBTUSB_BCM=y
CONFIG_BT_HCIBTUSB_RTL=y
CONFIG_BT_HCIBTUSB_MTK=y
```

`btusb` probes on USB enumeration — no attach step, independent of the
serdev path above. With both configured, onboard and USB radios coexist
and `desired_adapter:` picks which one this library drives.

## Device tree (Raspberry Pi)

The Pi systems must keep the Bluetooth child node under the mini-UART
(the `miniuart-bt` arrangement the upstream Nerves Pi systems already
use — BT on `/dev/ttyS0`, leaving the PL011 for the console). The serdev
kernel path binds that node and auto-attaches the chip; nothing in
userspace touches the UART.

Known boot-timing caveat on the Pi 3 B+: the serdev probe can race
rootfs availability, so the onboard radio occasionally enumerates
seconds late. See the adapter-selection notes in the
[architecture guide](architecture.md) for how that interacts with
`desired_adapter:`.

## App-side rootfs overlay

These live in the *application's* `rootfs_overlay` (not the system),
since they're policy, not plumbing:

- **`/var/lib/bluetooth → /data/bluetooth` symlink.** `bluetoothd`
  persists adapter identity and link keys under `/var/lib/bluetooth`,
  which is read-only squashfs on Nerves. `Bluez.prepare_runtime/0`
  creates `/data/bluetooth` (mode `0700`) at start; the symlink makes
  the daemon land there.

  ```text
  rootfs_overlay/var/lib/bluetooth -> /data/bluetooth
  ```

- **`/etc/bluetooth/main.conf`** (optional but recommended for passive
  scanning): BlueZ's default AdvMonitor scan duty cycle is aggressive
  (~50%), and every received advert becomes a `PropertiesChanged` signal
  your BEAM must D-Bus-decode — at ~50% duty this can dominate CPU on a
  small device. A ~10% duty cycle cuts the advert rate ~5× with no
  practical loss for sensor scanning:

  ```ini
  [LE]
  ScanIntervalAdvMonitor=480   # 300 ms
  ScanWindowAdvMonitor=48      # 30 ms
  ```

  (bluetoothd 5.79 logs a cosmetic "Unknown key" warning for these — a
  key-validation whitelist typo upstream; the values are applied, and
  the warning disappears on later BlueZ versions.)

## What the library handles itself

No system/overlay work is needed for: `/run/dbus` + the bus socket +
machine-id (created/cleaned by `Bluez.prepare_runtime/0`), daemon
supervision (`MuonTrap`, no init scripts — do **not** enable the
buildroot dbus/bluetoothd init scripts), or agent/monitor registration.

## Checklist

A new target is ready when, on hardware:

1. `ls /sys/class/bluetooth` shows `hci0` shortly after boot (serdev or
   btusb did its job).
2. `Bluez` starts and `Bluez.Client.adapters_info/0` lists the adapter
   with its MAC.
3. Passive scanning delivers adverts to your `on_advertisement:` fun.
4. With `audio: true`: `bluealsad` stays up (check the logs) and
   `Bluez.BlueAlsa.pcms/0` answers `[]` (not an exit) with no headset
   connected.