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:
|
|
Let’s import it and open a connection to the D-Bus:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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.
|
|
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.
|
|
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.
|
|
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
:
|
|
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.
|
|
Just like the service we are going to expose the properties of our Characteristic objects:
|
|
We now need to implement the methods of org.bluez.GattCharacteristic1
.
They will be overriden by the inherited classes:
|
|
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
.
|
|
To receive the glass order, we can create the GlassCharacteristic
as writeable:
|
|
Finally, we can see how we send notifications to the final
user with the DoorCharacteristic
:
|
|
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:
|
|
We can now search the org.bluez.GattManager1
interface on the adapter
and call its RegisterApplication
method:
|
|
Advertisement
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 aRegisterAdvertisement
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:
|
|
We are left with two methods to implement:
GetAll
to let BlueZ fetch the properties of our objectRelease
as defined inorg.bluez.LEAdvertisement
|
|
We can now register the advertisement object just like we did for the application:
|
|
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:
|
|
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:
|
|
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.
Links
- BlueZ Website
- BlueZ Documentation
- BlueZ Repository
- Albert Huang (2008) - https://people.csail.mit.edu/albert/bluez-intro/index.html
- For a C project, look at bluez_inc
- Official Rust bindings for BlueZ (well-maintained): bluer
- Szymon Janc (Embedded Linux Conference 2016): YouTube Video