Skip to content

Devilish Bluez

Martijn van Welie edited this page May 10, 2022 · 13 revisions

Tricky issues when building any serious application with Bluez:

Discovering devices

Discovery is one of the most complex things in Bluez. This is ironic because this complexity is caused by the mapping to DBus rather the discovery itself. When you call 'StartDiscovery' the following happens:

  • when an advertising packet is found, Bluez checks if the device is already present on the DBus
  • if it is not present yet, the object is created on the DBus and an InterfaceAdded signal is sent which contains all the properties known about that device at this point
  • if the device is already present, e.g. because it is bonded or because it was already created, Bluez sends a PropertyChanged signal.
  • when the advertising stops, Bluez will remove the device from the DBus after 30 seconds.

Ok, that may seem not so bad; just listen to InterfaceAdded and PropertiesChanged signals and we are done, right? WRONG!

The problem is that InterfaceAdded and PropertyChanged are broadcasted to anybody that is using Bluez on your system. So if there is another app doing BLE as well, you will receive signals 'caused' by that app as well. You can't say in Bluez 'only give me signals for things that my application did'. The signals are system wide and they are undirected broadcasts. So you need to deal with this in your code and know whether you are scanning and how to interpret the signals you receive.

The Next problem is that not every PropertyChanged signal is the result of a discovery. Once you connect to device you will also receive lots of PropertyChanged signals. So if you have an application that connects to various devices while scanning for more devices, you will need some logic to see which PropertyChanged signals are because of the discovery and which ones are because of connected devices. Good luck with that...

Lastly, how do you know you are scanning? The adapter has a property Discovering so you might decide to look at that. But that property only indicates if the adapter is discoverying. It might not be your app that started the discovery! The adapter is a shared resource for all applications and is either discovering or not. Bluez has introduced the concept of 'discovery sessions' to make sure multiple applications can start and stop a discovery. The way that is works is as follows:

  • Suppose the adapter is not scanning
  • Application A calls StartDiscovery and the adapter starts a discovery. Application A now has a 'session'
  • Application B calls StartDiscovery but since the adapter was already discovering, no changes to the adapter. Application B now also has a 'session' and there are 2 discovery sessions in total.
  • Application A calls StopDiscovery but the adapter keeps on discovering because there is still a session for Application B.
  • Application A will keep on receiving InterfaceAdded/PropertyChanged signals if it remained subscribed to those.
  • Application B calls StopDiscovery and since it had the last session, the adapter will not stop discovery

On top of that we have to understand discovery filters! Each application can set its own discovery filters. Bluez 'merges' those filters so that all devices are discovered. Since the adapter is a shared resource, merging discovery filters will lead to a widening of the filter critera:

  • Application A sets a filter for UUID 1
  • Application B sets a filter for UUID 2
  • Bluez will now allow results that match either UUID 1 or UUID 2
  • All applications will receive signals matching either filter.

Still think discovery is easy?

In BINC I implemented the following logic:

  • The adapter class listens to 'interface-added' signals for the adapter and 'properties-change' for devices
  • The adapter class sets a discovery filter with some service UUIDs. It stores a copy of the filter for later checks.
  • When the method call 'StartDiscovery' succeeds, assume we have a 'session'. Ignore the adapter property 'discovering'.
  • If we don't have a session, then ignore all 'interface-added' signals.
  • If we don't have a session, then ignore all 'properties-change' signals from unconnected devices.
  • Deal with 'properties-changed' signals only for devices that we connected ourselves.
  • If we have a session and we get a 'interface-added' signal, we double-check if it matches the discovery filter because other apps may have 'widened' the filter by setting less restrictive filters. If it matches the discovery filter we have found a valid discovery result.
  • If we have a session and get a 'properties-changed' signal, we check a bunch of conditions to see if it is a discovery result: 1) must be for a disconnected device, 2) must match the scan filter, 3) the property must be RSSI, ServiceData or ManufacturerData (as those are the only properties that can change during a discovery). If all conditions are satisfied, it is a valid discovery result.
  • When we call 'StopDiscovery' and succeeds, we assume the session is stopped.

To summarize, dealing with a discovery session is very complicated, especially if you want to make it rocksolid and take into account that other apps on your computer might also be doing BLE.

An interface has been added, is it a peripheral or a central?

When you start a discovery, Bluez will add new devices to the dbus and emit the InterfaceAdded signal. However, that is not the only reason an InterfaceAdded signal is sent. If your application acts as a peripheral and a external central connects to you, that device is also added to the dbus and an InterfaceAdded signal is sent. So how do you know if the InterfaceAdded signal is for a remote peripheral or a remote central?

Well, there is not property to indicate if a device is acting as a peripheral or central. The only thing possible is to use heuristics to determine if it is a central or not. A central has some uniqueness to it:

  • It doesn't have a RSSI value, because it connected straight away and wasn't scanned.
  • It also doesn't have any other properties you normally find by scanning like a service UUID or manufacturer data.

So by setting up some checks you can try to figure out if it was a central or not. Not ideal....

Notifications and attribute caching

Bluez aggressively caches attribute values, i.e. the value of characteristic or descriptor. According to the Bluetooth standard this should only be done for bonded devices, but Bluez does it for non-bonded devices as well. This is problematic. For example, if you connect to a device, call startNotify on a characteristic, disconnect and immediately connect again, then Bluez will assume that the startNotify is still valid. However, most devices don't support this and need the CCC descriptor to be written again.

The workaround for this is to immediately remove the device from the DBUS after a disconnect. Then Bluez will 'forget' the state of the descriptor and you can safely use startNotify again to turn on notifications. However, obviously this still doesn't help for bonded devices but the Bluetooth specs say that a device should be able to remember the state. Unfortunately not all devices implement the spec correctly. So it is best to cal stopNotify as soon as you are done with a characteristic.

Automatically connecting to a known device

On iOS and Android you can ask the BLE stack to 'automatically connect' to a known device. Internally the BLE stack then scans for the device and when it sees it. In Bluez this is possible for Bluetooth Classic devices like headsets but not for BLE devices. Hence, it means you need to manually keep on scanning to look for devices you are interested in. Not very handy...

All calls to DBus must be async

When issuing a call to the DBus you can choose to make a synchronous blocking call or an asynchronous call. Most people start out with doing synchronous calls and even some libraries do this. However, this will get you into trouble sooner or later. The problem is that signals and callbacks will come in on the same thread as you are doing your DBus calls. So if you do a blocking call you cannot handle signals or deal with pairing requests. So the safest is do only asynchronous calls. Alternatively you could do complex thread management to avoid this issue but that is more complex to do.

You cannot notify to a particular 'central' when implementing a peripheral

Signals on the DBus are broadcasts and cannot be sent to one particular receiver. So when you are implementing the peripheral role and send a notification or indication, you have to send a 'PropertiesChanged signal'. However, this will then go to all connected centrals and you cannot work around that. Perhaps not the biggest issue when implementing the peripheral role, but it is strange that something this basic is not possible whereas it is perfectly possible on other platforms.

Documentation is very very very basic

I have spent a lot of time figuring out how to use Bluez and the DBus apis. I can tell from experience that the documentation is very sparse and only understandable for people that are intimately familiar with the DBus and Bluez. Not only is it sparse it is also incomplete or difficult to understand, e.g.:

  • Try figuring out what the 'trusted' property of a Device is! Let me know if you figure it out as I still don't get it.
  • What on earth is 'pathloss'? Another property of a Device....that is definitely not a BLE thing.
  • According to the documentation there are options for the 'WriteValue' command but some are only relevant when doing the read and others only when receiving the read. Test question: will you receive a value for the 'type' option when handling a WriteValue call (as a peripheral)?
  • When implementing a peripheral, how do you send a confirmation for a 'write with response' request?
  • It is not very documented anywhere how to implement a peripheral, apart from the explanation that you need to 'register an object hierarchy and implement the 'ObjectManager' interface.
  • ...and so on.