BlueZ 5.50 and D-Bus

How BlueZ, D-Bus, and our application code work together.

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:

  1. Process A can send a message to Process B
  2. Process C can broadcast a message for any process that wants to tune in
  3. 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:

    • from dasbus.typing import *
    • @dbus_interface("org.bluez.GattCharacteristic1")
    • @emits_properties_changed
    • @property

    Dasbus typing

    dasbus.typing makes the glib types available to our application. We'll use this to create Variants 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  

    Next