Bluetooth, a wireless communication standard that exists since 1998, now equips more than 5 billions devices in the world. From your wireless mouse to your smart fridge, Bluetooth is everywhere. One may be wondering what Linux, one of the most used platform, is proposing to create Bluetooth services. This article will present the offical Linux Bluetooth API: BlueZ.

Now, for the sake of context, let’s imagine that you are working for a big tech company selling smart fridges.

The company is working on the next best-seller in the fridge industry, the Climafood, equipped with a big touch screen, network and Bluetooth capabilities. To save a lot of time, the hardware team went for the Raspberry Pi Compute Module 4 as the main board. You are the one in charge for developing the Bluetooth service.

The Climafood user should be able to:

  • read the content of the fridge.
  • be notified when the fridge door has been opened for more than 30 seconds.
  • request a glass of water with a specific number of ice cubes.

The job needs to be done for tomorrow so you are free to choose Python as the main language (as a bad idea as it is).

First, let’s see some key concepts to work with BlueZ.

bluetoothd

bluetoothd is the daemon responsible for managing Bluetooth devices. It is used to interact with the Bluetooth hardware and software.

This daemon acts as a D-Bus service, providing interfaces, objects and signals to develop Bluetooth applications easily. bluetoothctl is an example for a program using BlueZ’s D-Bus service.

A reminder on D-Bus

If you do not know D-Bus or feel the need for a reminder, this part will describe the system. D-Bus is a message bus system developed by the freedesktop.org project to propose a form of inter-process communication mechanism for GNOME and KDE Desktop environments. The project also gives an implementation of its own specification: libdbus. But in this article, we will be using a Python package using the GLib implementation of the protocol: GDBus.

The D-Bus specification is based on an object model. A D-Bus service such as bluetoothd exposes objects which have interfaces. These interfaces contains methods that may be called, and properties that may be read or modified by a client. These objects also may emit signals to communicate directly to a client that subscribed to them. This is especially useful for clients to get notified of state changes.

Objects are related to a specific D-Bus connection, which means that clients do not have to bother about concurrency.

Finally, objects are defined by their path, an unique way to represent an object. An example of a valid path would be /org/example/my_object.

A reminder on Bluetooth Low Energy

This part proposes only a few reminders about the Bluetooth Low Energy specification.

Please note that this is not a complete description and only contains the strict minimum to understand the rest of the article.

The Bluetooth 4.0 specification (2010) is widely known to have brought Bluetooth Low Energy (BLE), spreading even more Bluetooth usage in IoT. It also added more capabilities, such as broadcasting.

Bluetooth “Classic”, also known as Bluetooth Basic Rate/Enhanced Data Rate (BR/EDR) is still widely used in applications requiring more data rate, such as headphones or wireless speakers.

Bluetooth LE has introduced the notion of GATT (Generic ATTribute Profile) Servers/Clients. A such server describes how a client may communicate with it. It exposes Services, which expose Characteristics themselves.

Characteristics

You can see a Characteristic as an endpoint on which you can read and/or write data. To gather data from a Characteristic you can either read from it or subscribe to be notified when data is available. You can also write to a Characteristic to change its value, if it is allowed.

For the following ten minutes, you are allowed to see a Characteristic as a HTTP endpoint. You can have a GET endpoint like you would have a readable Characteristic, or a POST endpoint would be similar to a writable one.

Services

A Service can be seen as a coherent group of Characteristics, meaning the Characteristics expose information related to a specific subject. Some services are standard, such as the Battery Service, which consists of multiple characteristics exposing the battery level of the connected device. For sure, you may create your own service and specify your own characteristics.

Roles

Bluetooth devices can act as two different roles: central and/or peripheral. A peripheral advertises its presence by broadcasting Advertisement Data. This data may be found by a central device which will attempt or not to connect to it.

An example of a peripheral would be your wireless mouse. Your computer, acting as a central, could find this mouse and connect to it. The mouse would typically have a GATT Server, exposing the HID over GATT Profile.

Low-level protocols

The communication between the host (computer) and the Bluetooth IC is standardized with HCI for Host Controller Interface. This standard provides common packets and commands for any integrated circuit. It is defined as a multi-layer architecture to only have to adapt some layers to be compliant with a specific product (for example one may use a USB layer instead of UART).

The host may then expose multiple sockets, such as RFCOMM, MGMT or L2CAP sockets. We will not go into details about sockets in this article. In our case, L2CAP is the socket that will be used under our feet. As its name states, it is designed for logical connection establishment. GATT is built on top of L2CAP.

BlueZ API

With the previous reminders, you are ready to develop the service.

Using D-Bus in Python

First, you need the dbus-python pip package:

1
python3 -m pip install dbus-python

Let’s import it and open a connection to the D-Bus:

1
2
3
4
5
6
import dbus
import dbus.mainloop.glib

dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)

bus = dbus.SystemBus()

We will explain later the mainloop part.

Finding a Bluetooth Adapter

An Adapter, in the BlueZ API, refers to any device (an onboard chip or a dongle) that has Bluetooth capabilities and is compatible with BlueZ. Your first encounter with the BlueZ API documentation would start [here] (https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/adapter-api.txt). This page describes the methods available on the org.bluez.Adapter1 interface of the org.bluez service.

From this page, we learn three things:

  • The service is org.bluez
  • The object path is in the form of [variable prefix]/{hci0,hci1,...}
  • The interface on this object is org.bluez.Adapter1

So the first thing we need to do is search for such an object. To see all managed object by the org.bluez service, we can call the GetManagedObjects D-Bus method (part of the ObjectManager interface) on the root / object. Please note that this interface is declared in the [D-Bus specification] (https://dbus.freedesktop.org/doc/dbus-specification.html) and is not specific to BlueZ.

If you are curious about the reply of this method, or if you want to manually verify the presence of this object, you may run in your shell:

1
2
dbus-send --system --print-reply --dest=org.bluez / \
 org.freedesktop.DBus.ObjectManager.GetManagedObjects

A /org/bluez/hci0 entry should appear if you have at least one Adapter on your machine. You should also be able to see that it implements the org.bluez.Adapter1 interface.

To verify the presence of this adapter in Python, you may write the following code:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
def get_adapter(bus):
    """
    Returns a BlueZ Adapter Object if hci0 is available.
    """

    bluez_obj = bus.get_object("org.bluez", '/')
    # Get a reference to the ObjectManager interface of /
    obj_manager = dbus.Interface(bluez_obj,
        'org.freedesktop.DBus.ObjectManager')
    # Verify that hci0 is a present Adapter
    if not obj_manager.GetManagedObjects().get("/org/bluez/hci0"):
        print("Bluetooth Adapter not found")
        return None

    # Return a reference to the BlueZ D-Bus hci0 object.
    return bus.get_object("org.bluez", "/org/bluez/hci0")

It is now the good time to check the available methods and properties on the Adapter1 interface in the documentation. The Powered one is interesting. I have personnaly never seen the adapter’s Powered property set to false by default, but we can never be too sure.

Set the property to true with the following piece of code:

1
2
3
4
# Get a reference to the Properties interface of adapter.
adapter_properties = dbus.Interface(adapter, "org.freedesktop.DBus.Properties")
# Set the property
adapter_properties.Set("org.bluez.Adapter1", "Powered", dbus.Boolean(1))

GATT Server

It is now time to describe the Bluetooth services running on the Climafood!

The GATT Server will consist of a single Service containing 3 Characteristics:

  • Content: Read fridge content
  • Door: Notify if door has been open for too long
  • Glass: Serve glass of water

As they are not standard, we need to generate UUIDs for the Service and its Characteristics. We will use an online generator.

We will use the following ones:

1
2
3
4
SERVICE_UUID="139fc001-a4ed-11ed-b9df-0242ac120003"
CONTENT_UUID="139fc002-a4ed-11ed-b9df-0242ac120003"
DOOR_UUID   ="139fc003-a4ed-11ed-b9df-0242ac120003"
GLASS_UUID  ="139fc004-a4ed-11ed-b9df-0242ac120003"

To describe the GATT application to BlueZ, we will create our own D-Bus objects implementing some BlueZ interfaces.

The documentation demonstrates well the hierarchy of the objects we have to create, and the interfaces they will implement:

-> /com/example # Application
  |   - org.freedesktop.DBus.ObjectManager
  |
  -> /com/example/service0 # Service
  | |   - org.freedesktop.DBus.Properties
  | |   - org.bluez.GattService1
  | |
  | -> /com/example/service0/char0 # Characteristic
  | |     - org.freedesktop.DBus.Properties
  | |     - org.bluez.GattCharacteristic1
  | |
  | -> /com/example/service0/char1 # Characteristic
  |   |   - org.freedesktop.DBus.Properties
  |   |   - org.bluez.GattCharacteristic1

Application

We are going to create our first D-Bus object: the application. We will put all of our objects under the path /climafood.

An application is just a list of services, which in our case only consists of the ClimafoodService described later.

1
2
3
4
5
class Application(dbus.service.Object):
    def __init__(self, bus):
        self.path = "/climafood/app" # Object path
        self.services = [ClimafoodService(bus)]
        dbus.service.Object.__init__(self, bus, self.path)

We then need to implement the GetManagedObjects method of the ObjectManager interface. This method will be called by BlueZ to gather information about all objects managed by our application.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
class Application(dbus.service.Object):
    ...

    @dbus.service.method(
        "org.freedesktop.DBus.ObjectManager", out_signature="a{oa{sa{sv}}}"
    )
    def GetManagedObjects(self):
        response = {}

        for service in self.services:
            response[dbus.ObjectPath(service.path)] = service.get_properties()
            chrcs = service.characteristics
            for chrc in chrcs:
                response[dbus.ObjectPath(chrc.path)] = chrc.get_properties()

        return response

We return a dictionnary containing the properties of every object under our application.

Do not bother too much with the out_signature parameter. It is the standard way in GLib to describe the type of an element.

Service

We now need to create the ClimafoodService object, which implements org.bluez.GattService1 (consisting of only properties) and org.freedesktop.DBus.Properties.

A Bluetooth Service has an UUID, a boolean to tell if the service is primary, and a list of Characteristics. Once again, we will define these later.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class ClimafoodService(dbus.service.Object):
    def __init__(self, bus):
        self.path = "/climafood/app/service"
        self.bus = bus
        self.characteristics = [ContentCharacteristic(bus, self)]
        dbus.service.Object.__init__(self, bus, self.path)

    def get_characteristic_paths(self):
        result = []
        for chrc in self.characteristics:
            result.append(dbus.ObjectPath(chrc.path))
        return result

    def get_properties(self):
        return {
            "org.bluez.GattService1": {
                "UUID": SERVICE_UUID,
                "Primary": True,
                "Characteristics": dbus.Array(
                    self.get_characteristic_paths(), signature="o"
                ),
            }
        }

The get_properties method is a simple Python method that helps the Application gathering information about the service. It also simplifies a lot the implemention of the GetAll method of org.freedesktop.DBus.Properties:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class ClimafoodService(dbus.service.Object):
    ...

    @dbus.service.method(
        "org.freedesktop.DBus.Properties", in_signature="s",
        out_signature="a{sv}"
    )
    def GetAll(self, interface):
        # Returning only org.bluez.GattService1 properties
        return self.get_properties()["org.bluez.GattService1"]

Characteristics

As we are going to describe three Characteristics, let’s first write a generic implementation of them.

A Characteristic consists of an UUID and flags describing its capabilities.

1
2
3
4
5
6
7
8
class Characteristic(dbus.service.Object):
    def __init__(self, bus, path, uuid, flags, service):
        self.path = path
        self.bus = bus
        self.uuid = uuid
        self.service = service
        self.flags = flags
        dbus.service.Object.__init__(self, bus, self.path)

Just like the service we are going to expose the properties of our Characteristic objects:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
class Characteristic(dbus.service.Object):
    ...

    def get_properties(self):
        return {
            "org.bluez.GattCharacteristic1": {
                "Service": dbus.ObjectPath(self.service.path),
                "UUID": self.uuid,
                "Flags": self.flags,
            }
        }

    @dbus.service.method(
        "org.freedesktop.DBus.Properties",
        in_signature="s", out_signature="a{sv}"
    )
    def GetAll(self, interface):
        return get_properties()["org.bluez.GattCharacteristic1"]

We now need to implement the methods of org.bluez.GattCharacteristic1. They will be overriden by the inherited classes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Characteristic(dbus.service.Object):
    ...

    @dbus.service.method(
        "org.bluez.GattCharacteristic1",
        in_signature="a{sv}", out_signature="ay"
    )
    def ReadValue(self, options):
        print("Default ReadValue called, returning error")
        raise NotSupportedException()

    @dbus.service.method(
        "org.bluez.GattCharacteristic1", in_signature="aya{sv}"
    )
    def WriteValue(self, value, options):
        print("Default WriteValue called, returning error")
        raise NotSupportedException()

    @dbus.service.method("org.bluez.GattCharacteristic1")
    def StartNotify(self):
        print("Default StartNotify called, returning error")
        raise NotSupportedException()

    @dbus.service.method("org.bluez.GattCharacteristic1")
    def StopNotify(self):
        print("Default StopNotify called, returning error")
        raise NotSupportedException()

Creating the ContentCharacteristic (displaying the fridge content) is now easy. We only have to handle the ReadValue method. It implies that the Characteristic flag is read.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class ContentCharacteristic(Characteristic):
    def __init__(self, bus, service):
        Characteristic.__init__(
            self,
            bus,
            "/climafood/app/service/content",
            CONTENT_UUID,
            ["read"],
            service,
        )

    def ReadValue(self, options):
        # Type needs to be a list of bytes
        return list(b"empty")

To receive the glass order, we can create the GlassCharacteristic as writeable:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class GlassCharacteristic(Characteristic):
    def __init__(self, bus, service):
        Characteristic.__init__(
            self,
            bus,
            "/climafood/app/service/glass",
            GLASS_UUID,
            ["write"],
            service,
        )

    def WriteValue(self, value, options):
        print(f"Preparing a glass of water with {int(value[0])} ice cubes.")

Finally, we can see how we send notifications to the final user with the DoorCharacteristic:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class DoorCharacteristic(Characteristic):
    def __init__(self, bus, service):
        Characteristic.__init__(
            self,
            bus,
            "/climafood/app/service/door",
            DOOR_UUID,
            ["notify"],
            service
        )

    def StartNotify(self):
        print("Client started to listen for notifications")
        # Example of sending a notification containing '30'
        self.PropertiesChanged("org.bluez.GattCharacteristic1",
            { 'Value': [dbus.Byte(30)] }, [])

    def StopNotify(self):
        print("Client stopped to listen for notifications")

For sure there is no logic here, you would need to start a timer once the door is open, and send a notification at the right time.

Final step: registering the application

Our application object and its content are ready to be registered on BlueZ.

We need to create two callbacks:

  • one for a successful registration
  • one for a failed registration

It is as simple as:

1
2
3
4
5
def app_register_success():
    print("Application registered successfully.")

def app_register_failure(error):
    print("Application failed to be registered: " + str(error))

We can now search the org.bluez.GattManager1 interface on the adapter and call its RegisterApplication method:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
app = Application(bus)

gatt_manager = dbus.Interface(adapter, "org.bluez.GattManager1")

gatt_manager.RegisterApplication(
    dbus.ObjectPath(app.path),
    {},
    reply_handler=app_register_success,
    error_handler=app_register_failure,
)

It is nice to have a GATT Server, but it needs to be found by the client’s phone. We need to advertise our Service so that the Climafood mobile application can search for its UUID on the Bluetooth network.

Thus, you are invited to read the [documentation] (https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/advertising-api.txt) related to advertisement.

What we can learn from the documentation is:

  • The Interface org.bluez.LEAdvertisingManager1 on /org/bluez/{hci0,hci1,...} has a RegisterAdvertisement method.
  • We need to create an object implementing the org.bluez.LEAdvertisement1.

We will not cover all the properties, only the strict minimum to have a discoverable device. If you want to learn more about each property, check the Bluetooth specification and then check how you must format the data on D-Bus.

Let’s create the advertisement object:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Advertisement(dbus.service.Object):
    def __init__(self, bus):
        self.path = "/climafood/advertising"
        self.bus = bus
        # Set Type
        self.adv_type = "peripheral"
        # Set Local Name
        self.local_name = "Climafood"
        # Set the advertised UUIDs (ClimafoodService)
        self.UUIDs = [SERVICE_UUID]
        dbus.service.Object.__init__(self, bus, self.path)

We are left with two methods to implement:

  • GetAll to let BlueZ fetch the properties of our object
  • Release as defined in org.bluez.LEAdvertisement
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
class Advertisement(dbus.service.Object):
    ...

    @dbus.service.method('org.freedesktop.DBus.Properties',
                         in_signature='s',
                         out_signature='a{sv}')
    def GetAll(self, interface):
        return {
            "Type": self.adv_type,
            "LocalName": dbus.String(self.local_name),
            "ServiceUUIDs": dbus.Array(self.UUIDs, signature='s')
        }

    @dbus.service.method("org.bluez.LEAdvertisement",
                         in_signature='',
                         out_signature='')
    def Release(self):
        # Nothing to do
        print("Advertisement released.")

We can now register the advertisement object just like we did for the application:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def adv_register_success():
    print("Advertisement registered successfully.")

def adv_register_failure(error):
    print("Advertisement failed to be registered: " + str(error))

adv = Advertisement(bus)

adv_manager_obj = dbus.Interface(adapter, "org.bluez.LEAdvertisingManager1")
adv_manager_obj.RegisterAdvertisement(
    adv.path, {}, reply_handler=adv_register_success,
    error_handler=adv_register_failure
)

Main loop

You may have executed your program and see that it exits immediately.

In fact, GLib is used behind the dbus-python package. In GLib, to keep your program running, but more importantly, to receive events, you need to run the [Main Event Loop] (https://docs.gtk.org/glib/main-loop.html).

To do it in Python, you will need the python3-gi package. You can now add the following lines at the end of your program:

1
2
3
4
from gi.repository import GObject

mainloop = GObject.MainLoop()
mainloop.run()

Testing

To verify if you can see your device, the nRF Connect mobile application contains very nice tools.

You should be able to find your device by its local name (in our case Climafood), connect to it and play with its characteristics.

Debugging

You are possibly not seeing your device on the mobile app, or you may have unexpected behaviors.

The best way to debug D-Bus errors is to monitor messages sent to and received from BlueZ.

Run this command in a shell and look for errors:

1
sudo dbus-monitor --system "destination='org.bluez'" "sender='org.bluez'"

Going further

Now let’s be honest, you would not write such an application in Python.

The goal of the article was to present you both BlueZ and D-Bus. Proposing such an article with examples written in C would have also required to explain GLib and its GDBus implementation. Furthermore, code examples would have contained a lot of boilerplate code due to C/GLib memory management.

If you want to go further with BlueZ in C, I strongly recommend you to check the source code of the bluez_inc project.

This article might have been a bit overhelming; I was too when I discovered BlueZ and D-Bus. The key is to familiarize yourself with the documentation and D-Bus concepts. Then, you will start to methodically look the documentation like so:

  • What are the BlueZ D-Bus objects related to my problem ?
    • What are their methods and properties ?
  • What are the D-Bus objects I need to create ?
    • What are the methods and properties I need to expose ?

In this article, we did not talk about other Bluetooth solutions, that manufacturers like Nordic may provide. Indeed, BlueZ might not be the best choice for an embedded solution. Your project might too constrainted to use both D-Bus and GLib.

Furthermore, BlueZ does not seem very stable when looking at the hundreds of issues on its repository, which maintainers seem to struggle with. Some of the basic features of BLE, like advertising interval, or TX power setting are still presented as experimental. As a personal note, I had to forget about using BlueZ in my project because it cannot, in its current state, send notifications to a specific device. Meaning you always send notifications to all connected centrals, which did not fit my need. To conclude, make sure BlueZ is adapted for your application. It is portable and has a high-level API, but has limited maintenance.

I wish you good luck in your journey with BlueZ and its API.