BlueZ 5.50 and D-Bus

To get our application working, it's helpful to understand how we're actually going to make the Bluetooth radio do things. The Bluetooth radio is controlled by a driver called BlueZ. BlueZ runs a daemon called bluetoothd If we want the radio to do something, we need to tell bluetoothd to make it happen. But, how does one running process(our app) tell another running process(bluetoothd) to do something? This is where D-Bus comes in.
D-Bus
D-Bus runs a daemon called dbus, that facilitates interprocess communication and remote procedure calls in a Linux system. D-Bus acts a little like a telephone operator:
- Process A can send a message to Process B
- Process C can broadcast a message for any process that wants to tune in
from dasbus.typing import *
@dbus_interface("org.bluez.GattCharacteristic1")
@emits_properties_changed
@property
Working with BlueZ we're going to make a lot of remote procedure calls from our application. For example: RegisterAdvertisement(object advertisement, dict options)
bluetoothd listens to dbus for RegisterAdvertisement
method calls. Our application is going to tell dbus to call the RegisterAdvertisement
method on bluetoothd.
This creates an interesting challenge (that we'll solve 😁). Methods usually have arguments and those arguments usually have some type: integer, string, array, etc. With D-Bus in the middle, how do the argument types get preserved?
This is where glib comes in. glib helps us map our python code to types that are supported by D-Bus. The following table was really helpful to me:
Type | Format String |
---|---|
Bool | "b" |
Str | "s" |
Double | "d" |
Int | "i" |
Byte | "y" |
Int16 | "n" |
UInt16 | "q" |
Int32 | "i" |
UInt32 | "u" |
ObjPath | "o" |
Variant | "v" |
The ObjPath
type deserves special attention. Object Paths look like strings, but they have special semantic meaning. An example Object Path is: /com/raspberrypi-bluetooth/Thermometer/Characteristic/1
. Object Paths look like file paths on the Linux file system, and they're one way that D-Bus identifies something that can send/receive messages.
D-Bus interfaces
Applications that interact with D-Bus publish a set of "interfaces" that define how to interact with them. For example, BlueZ publishes the org.bluez.LEAdvertisement1
interface for defining Bluetooth Low Energy advertisements. It looks like this:
Service org.bluez
Interface org.bluez.LEAdvertisement1
Object path freely definable
Methods void Release() [noreply]
This method gets called when the service daemon
removes the Advertisement. A client can use it to do
cleanup tasks. There is no need to call
UnregisterAdvertisement because when this method gets
called it has already been unregistered.
Properties string Type
Determines the type of advertising packet requested.
Possible values: "broadcast" or "peripheral"
array{string} ServiceUUIDs
List of UUIDs to include in the "Service UUID" field of
the Advertising Data.
... more properties ...
A note on interfaces: documentation on all of the intefaces that BlueZ exposes to our application, and expects from it are in the /docs
folder of the BlueZ source code.
This tells us that if our code is going to conform to the org.bluez.LEAdvertisement1
interface, which allows BlueZ to ask for it's properties, and call the Release
method, our application needs to implement that method and the defined properties.
A very common interface is org.freedesktop.DBus.Properties
. This interface allows the getting and setting of properties:
org.freedesktop.DBus.Properties.Get (in STRING interface_name,
in STRING property_name,
out VARIANT value);
org.freedesktop.DBus.Properties.Set (in STRING interface_name,
in STRING property_name,
in VARIANT value);
org.freedesktop.DBus.Properties.GetAll (in STRING interface_name,
out DICT props);
By using the Python library dasbus we get this for free.
Dasbus
Our application is going to be written in Python, and we're going to use the dasbus library to interact with D-Bus. The key topics for dasbus are the typing, and some decorators:
Dasbus typing
dasbus.typing
makes the glib types available to our application. We'll use this to create Variant
s and allow Dasbus to introspect our code for interfaces that we create.
# Tell BlueZ that we don't want our peripheral to be pairable, this way the central
# doesn't attempt to show the "would like to pair" dialog to the user. A Bluetooth
# Low Energy peripheral doesn't need to pair to communicate with the central.
#
proxy = bus.get_proxy("org.bluez", "/org/bluez/hci0")
proxy.Set("org.bluez.Adapter1", "Pairable", Variant("b", False))
The Variant("b", False)
section is how we convert Python's False
into a boolean value that D-Bus can work with.
Bluetooth Characteristics created by our application need to conform to the org.bluez.GattCharacteristic1
interface, which has a ReadValue
method. ReadValue
has one positional parameter (options
) that accepts a dictionary with string keys, and variant values. Those variant values can be any valid GLib type. ReadValue
returns an array of bytes.
@dbus_interface("org.bluez.GattCharacteristic1")
class CharacteristicInterfaceGatt(InterfaceTemplate):
def ReadValue(self, options: Dict[Str, Variant]) -> List[Byte]:
print("TRIED TO READ VALUE")
return self.implementation.value
Using Python's type hints makes it possible for Dasbus to introspect our code and to tell D-Bus the method signature for our ReadValue
method.
Dasbus decorators
The three decorators are going to instruct Dasbus to communicate some more things about our code with D-Bus.
@dbus_interface("org.bluez.GattCharacteristic1")
is going to let D-Bus know that our application implements the org.bluez.Characteristic1
interface, and that processes communicating with our app can access the methods and properties that it defines.
@emits_properties_changed
tells D-Bus that when this method is called, BlueZ should notify centrals that have subscribed to notifications about the new value.
@property
is not actually provided by Dasbus, it's part of Python, but Dasbus will use this method decorator to publish properties that can be set or read on our application.
The decorators come together like this:
# This class has the methods and properties required by the GattCharacteristic1 interface
@dbus_interface("org.bluez.GattCharacteristic1")
class CharacteristicInterfaceGatt(InterfaceTemplate):
# Notify listeners that `Value` has a new value
@emits_properties_changed
def WriteValue(self, value: List[Byte], options: Dict[Str, Str]) -> None:
self.implementation.value = value
self.report_changed_property('Value')
# Other processes on D-Bus can read our `UUID`
@property
def UUID(self) -> Str:
return Characteristic.UUID