You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Noble's HCI binding has persistent issues with stateChange not firing reliably on Linux (see #66). The root cause is that the HCI user-channel binding requires exclusive adapter access, which conflicts with BlueZ. In Docker containers, the problem is compounded: /dev/hciX devices may not exist, hci-ble fails silently, and there is no fallback.
We have built a BlueZ D-Bus binding for Noble that resolves these issues by using BlueZ's native D-Bus API (org.bluez) instead of raw HCI.
Problems with the HCI binding on Linux
stateChange not triggered ('stateChange' is not reliably triggered on Raspberry Pi 4 #66): The HCI binding opens a user-channel socket which requires exclusive adapter access. If BlueZ is managing the adapter (which it is by default on every systemd-based distro), the socket open either fails silently or BlueZ and Noble fight over the adapter state.
Docker incompatibility: In containers, /dev/hciX often doesn't exist even with --privileged or device passthrough. The HCI binding's Hci.init() fails before emitting any state, leaving consumers waiting forever for poweredOn.
No graceful degradation: When HCI binding initialisation fails, Noble doesn't fall back to an alternative. The consumer gets no stateChange event and no error — it just hangs.
Our solution: BlueZ D-Bus binding
We implemented a D-Bus binding (lib/bluez-dbus/) that talks to BlueZ via org.bluez over the system D-Bus socket. This:
Uses BlueZ natively: No exclusive adapter access needed. Works alongside BlueZ, bluetoothctl, and other D-Bus clients.
Works in Docker: Only needs the D-Bus system socket mounted (/run/dbus/system_bus_socket). No /dev/hciX device nodes required.
Reliable state detection: Reads adapter Powered property via D-Bus org.bluez.Adapter1. State changes are delivered via D-Bus PropertiesChanged signals — no polling, no race conditions.
Full scanning and connection support: Uses org.bluez.Adapter1.StartDiscovery / StopDiscovery for scanning, org.bluez.Device1.Connect / Disconnect for connections, and org.bluez.GattService1 / GattCharacteristic1 for GATT operations.
bab31d4 feat(bluez-dbus): add binding selection to resolve-bindings
Binding selection
The D-Bus binding is selected automatically when resolve-bindings detects Linux and an available D-Bus socket. The HCI binding remains available as a fallback for systems without BlueZ (embedded, custom kernels).
Testing
Tested extensively with Matter device commissioning (Nanoleaf NL75) in Docker on NixOS. BlueZ 5.79, kernel 6.12. Scanning, connecting, GATT discovery, and characteristic read/write all work reliably.
Happy to contribute this upstream if there's interest — the binding is fairly self-contained in lib/bluez-dbus/.
Summary
Noble's HCI binding has persistent issues with
stateChangenot firing reliably on Linux (see #66). The root cause is that the HCI user-channel binding requires exclusive adapter access, which conflicts with BlueZ. In Docker containers, the problem is compounded:/dev/hciXdevices may not exist,hci-blefails silently, and there is no fallback.We have built a BlueZ D-Bus binding for Noble that resolves these issues by using BlueZ's native D-Bus API (
org.bluez) instead of raw HCI.Problems with the HCI binding on Linux
stateChangenot triggered ('stateChange' is not reliably triggered on Raspberry Pi 4 #66): The HCI binding opens a user-channel socket which requires exclusive adapter access. If BlueZ is managing the adapter (which it is by default on every systemd-based distro), the socket open either fails silently or BlueZ and Noble fight over the adapter state.Docker incompatibility: In containers,
/dev/hciXoften doesn't exist even with--privilegedor device passthrough. The HCI binding'sHci.init()fails before emitting any state, leaving consumers waiting forever forpoweredOn.No graceful degradation: When HCI binding initialisation fails, Noble doesn't fall back to an alternative. The consumer gets no
stateChangeevent and no error — it just hangs.Our solution: BlueZ D-Bus binding
We implemented a D-Bus binding (
lib/bluez-dbus/) that talks to BlueZ viaorg.bluezover the system D-Bus socket. This:bluetoothctl, and other D-Bus clients./run/dbus/system_bus_socket). No/dev/hciXdevice nodes required.Poweredproperty via D-Busorg.bluez.Adapter1. State changes are delivered via D-BusPropertiesChangedsignals — no polling, no race conditions.org.bluez.Adapter1.StartDiscovery/StopDiscoveryfor scanning,org.bluez.Device1.Connect/Disconnectfor connections, andorg.bluez.GattService1/GattCharacteristic1for GATT operations.Architecture
Key commits
Our fork:
poodle64/nobled9de461feat: add dbus-next dependency for BlueZ D-Bus binding6427cf4feat(bluez-dbus): add utils module03e9c6afeat(bluez-dbus): add adapter lifecycle modulebb715e4feat(bluez-dbus): add scanner module1ba8e8bfeat(bluez-dbus): add device connection manager4be24fffeat(bluez-dbus): add GATT managerabfec53feat(bluez-dbus): add main binding modulebab31d4feat(bluez-dbus): add binding selection to resolve-bindingsBinding selection
The D-Bus binding is selected automatically when
resolve-bindingsdetects Linux and an available D-Bus socket. The HCI binding remains available as a fallback for systems without BlueZ (embedded, custom kernels).Testing
Tested extensively with Matter device commissioning (Nanoleaf NL75) in Docker on NixOS. BlueZ 5.79, kernel 6.12. Scanning, connecting, GATT discovery, and characteristic read/write all work reliably.
Happy to contribute this upstream if there's interest — the binding is fairly self-contained in
lib/bluez-dbus/.