EventGet 50% off your ticket to MongoDB.local NYC on May 2. Use code Web50!Learn more >>
MongoDB Developer
C++
plus
Sign in to follow topics
MongoDB Developer Centerchevron-right
Developer Topicschevron-right
Languageschevron-right
C++chevron-right

Me and the Devil BlueZ: Reading BLE sensors from C++

Jorge D. Ortiz-Fuentes16 min read • Published Jan 09, 2024 • Updated Mar 18, 2024
RaspberryPiC++
Facebook Icontwitter iconlinkedin icon
Rate this tutorial
star-empty
star-empty
star-empty
star-empty
star-empty

Me and the Devil Bluez: Reading BLE sensors from C++

In our last article, I shared how to interact with Bluetooth Low Energy devices from a Raspberry Pi with Linux, using DBus and BlueZ. I did a step-by-step walkthrough on how to talk to a BLE device using a command line tool, so we had a clear picture of the sequence of operations that had to be performed to interact with the device. Then, I repeated the process but focused on the DBus messages that have to be exchanged to achieve that interaction.
Now, it is time to put that knowledge into practice and implement an application that connects to the RP2 BLE sensor that we created in our second article and reads the value of the… temperature. (Yep, we will switch to noise sometime soon. Please, bear with me.)
Ready to start? Let's get cracking!

Setup

The application that we will be developing in this article is going to run on a Raspberry Pi 4B, our collecting station. You can use most other models, but I strongly recommend you connect it to your network using an ethernet cable and disable your WiFi. Otherwise, it might interfere with the Bluetooth communications.
I will do all my development using Visual Studio Code on my MacBook Pro and connect via SSH to the Raspberry Pi (RPi). The whole project will be held in the RPi, and I will compile it and run it there. You will need the Remote - SSH extension installed in Visual Studio Code for this to work, and the first time you connect to the RPi, it will take some time to set it up. If you use Emacs, TRAMP is available out of the box.
We also need some software installed on the RPi. At the very least, we will need git and CMake, because that is the build system that I will be using for the project. The C++ compiler (g++) is installed by default in Raspberry Pi OS, but you can install Clang if you prefer to use LLVM.
In any case, we will need to install sdbus-c++. That is the library that allows us to interact with DBus using C++ bindings. There are several alternatives, but sdbus-c++ is properly maintained and has good documentation.

Initial project

I am going to write this project from scratch, so I want to be sure that you and I start with the same set of files. I am going to begin with a trivial main.cpp file, and then I will create the seed for the build instructions that we will use to produce the executable throughout this episode.

Initial main.cpp

Our initial main.cpp file is just going to print a message:

Basic project

And now we should create a CMakeLists.txt file with the minimal build instructions for this project:
Before we move forward, we are going to check that it all works fine:

Talk to DBus from C++

Send the first message

Now that we have set the foundations of the project, we can send our first message to DBus. A good one to start with is the one we use to query if the Bluetooth radio is on or off.
  1. Let's start by adding the library to the project using CMake's find_package command:
  2. The library must be linked to our binary:
  3. And we enforce the usage of the C++17 standard because it is required by the library:
  4. With the library in place, let's create the skeleton to implement our BLE sensor. We first create the BleSensor.h file:
  5. We add a constructor and a method that will take care of all the steps required to scan for and connect to the sensor:
  6. In order to talk to BlueZ, we should create a proxy object. A proxy is a local object that allows us to interact with the remote DBus object. Creating the proxy instance without passing a connection to it means that the proxy will create its own connection automatically, and it will be a system bus connection.
  7. And we need to include the library:
  8. Let's create a BleSensor.cpp file for the implementation and include the header file that we have just created:
  9. That proxy requires the name of the service and a path to the instance that we want to talk to, so let's define both as constants inside of the constructor:
  10. Let's add the first step to our scanAndConnect method using a private function that we declare in the header:
  11. Following this, we write the implementation, where we use the proxy that we created before to send a message. We define a message to a method on an interface using the required parameters, which we learned using the introspectable interface and the DBus traces. The result is a variant that can be casted to the proper type using the overloaded operator():
  12. We use this private method from our public one:
  13. And include the iostream header:
  14. We need to add the source files to the project:
  15. Finally, we import the header that we have defined in the main.cpp, create an instance of the object, and invoke the method:
  16. We compile it with CMake and run it.

Send a second message

Our first message queried the status of a property. We can also change things using messages, like the status of the Bluetooth radio:
  1. We declare a second private method in the header:
  2. And we also add it to the implementation file –in this case, only the message without the constants:
  3. As you can see, the calls to create and send the message use most of the same constants. The only new one is the METHOD_SET, used instead of METHOD_GET. We set that one inside of the method:
  4. And we make the other three static constants of the class. Prior to C++17, we would have had to declare them in the header and initialize them in the implementation, but since then, we can use inline to initialize them in place. That helps readability:
  5. With the private method complete, we use it from the public one:
  6. The second message is ready and we can build and run the program. You can verify its effects using bluetoothctl.

Deal with signals

The next thing we would like to do is to enable scanning for BLE devices, find the sensor that we care about, connect to it, and disable scanning. Obviously, when we start scanning, we don't get to know the available BLE devices right away. Some reply almost instantaneously, and some will answer a little later. DBus will send signals, asynchronous messages that are pushed to a given object, that we will listen to.

Use messages that have a delayed response

  1. We are going to use a private method to enable and disable the scanning. The first thing to do is to have it declared in our header:
  2. In the implementation file, the method is going to be similar to the ones we have defined before. Here, we don't have to worry about the reply because we have to wait for our sensor to show up:
  3. We can then use that method in our public one to enable and disable scanning:
  4. We need to wait for the devices to answer, so let's add some delay between both calls:
  5. And we add the headers for this new code:
  6. If we build and run, we will see no errors but no results of our scanning, either. Yet.

Subscribe to signals

In order to get the data of the devices that scanning for devices produces, we need to be listening to the signals sent that are broadcasted through the bus.
  1. We need to interact with a different DBus object so we need another proxy. Let's declare it in the header:
  2. And instantiate it in the constructor:
  3. Next, we define the private method that will take care of the subscription:
  4. The implementation is simple: We provide a closure to be called on a different thread every time we receive a signal that matches our parameters:
  5. The closure has to take as arguments the data that comes with a signal: a string for the path that points to an object in DBus and a dictionary of key/values, where the keys are strings and the values are dictionaries of strings and values:
  6. We will be doing more with the data later, but right now, displaying the thread id, the object path, and the device name, if it exists, will suffice. We use a regular expression to restrict our attention to the Bluetooth devices:
  7. And we add the header for regular expressions:
  8. We use the private method before we start scanning:
  9. And we print the thread id in that same method:
  10. If you build and run this code, it should display information about the BLE devices that you have around you. You can show it to your friends and tell them that you are searching for spy microphones.

Communicate with the sensor

Well, that looks like progress to me, but we are still missing the most important features: connecting to the BLE device and reading values from it.
We should connect to the device, if we find it, from the closure that we use in subscribeToInterfacesAdded(), and then, we should stop scanning. However, that closure and the method scanAndConnect() are running in different threads concurrently. When the closure connects to the device, it should inform the main thread, so it stops scanning. We are going to use a mutex to protect concurrent access to the data that is shared between those two threads and a conditional variable to let the other thread know when it has changed.

Connect to the BLE device

  1. First, we are going to declare a private method to connect to a device by name:
  2. We will obtain that object path from the signals that tell us about the devices discovered while scanning. We will compare the name in the dictionary of properties of the signal with the name of the sensor that we are looking for. We'll receive that name through the constructor, so we need to change its declaration:
  3. And declare a field that will be used to hold the value:
  4. If we find the device, we will create a proxy to the object that represents it:
  5. We move to the implementation and start by adapting the constructor to initialize the new values using the preamble:
  6. We then create the method:
  7. We create a proxy for the device that we have selected using the name:
  8. And move the declaration of the service constant, which is now used in two places, to the header:
  9. And send a message to connect to it:
  10. We define the constants that we are using:
  11. And the closure that will be invoked. The use of this in the capture specification allows access to the object instance. The code in the closure will be added below.
  12. The private method can now be used to connect from the method BleSensor::subscribeToInterfacesAdded(). We were already extracting the name of the device, so now we use it to connect to it:
  13. We would like to stop scanning once we are connected to the device. This happens in two different threads, so we are going to use the producer-consumer concurrency design pattern to achieve the expected behavior. We define a few new fields –one for the mutex, one for the conditional variable, and one for a boolean flag:
  14. And we include the required headers:
  15. They are initialized in the constructor preamble:
  16. We can then use these new fields in the BleSensor::scanAndConnect() method. First, we get a unique lock on the mutex before subscribing to notifications:
  17. Then, between the start and the stop of the scanning process, we wait for the conditional variable to be signaled. This is a more robust and reliable implementation than using the delay:
  18. In the connectionCallback, we first deal with errors, in case they happen:
  19. Then, we get a lock on the same mutex, change the flag, release the lock, and signal the other thread through the connection variable:
  20. Finally, we change the initialization of the BleSensor in the main file to pass the sensor name:
  21. If we compile and run what we have so far, we should be able to connect to the sensor. But if the sensor isn't there, it will wait indefinitely. If you have problems connecting to your device and get "le-connection-abort-by-local," use an ethernet cable instead of WiFi and disable it with sudo ip link set wlan0 down.

Read from the sensor

Now that we have a connection to the BLE device, we will receive signals about other interfaces added. These are going to be the services, characteristics, and descriptors. If we want to read data from a characteristic, we have to find it –using its UUID for example– and use DBus's "Read" method to get its value. We already have a closure that is invoked every time a signal is received because an interface is added, but in this closure, we verify that the object path corresponds to a device, instead of to a Bluetooth attribute.
  1. We want to match the object path against the structure of a BLE attribute, but we want to do that only when the device is already connected. So, we surround the existing regular expression match:
  2. In the else part, we add a different match:
  3. That code requires the regular expression declared in the method:
  4. If the path matches the expression, we check if it has the UUID of the characteristic that we want to read:
  5. When we find the desired characteristic, we need to create (yes, you guessed it) a proxy to send messages to it.
  6. That proxy is stored in a field that we haven't declared yet. Let's do so in the header file:
  7. And we do an explicit initialization in the constructor preamble:
  8. Everything is ready to read, so let's declare a public method to do the reading:
  9. And a private method to send the DBus messages:
  10. We implement the public method, just using the private method:
  11. And we do the implementation on the private method:
  12. We define the constants that we used:
  13. And the variable that will be used to qualify the query to have a zero offset as well as the one to store the response of the method:
  14. The temperature starts on the second byte of the result (offset 1) and ends on the fifth, which in this case is the last one of the array of bytes. We can extract it:
  15. Those bytes in ieee11073 format have to be transformed into a regular float, and we use a private method for that:
  16. That method is implemented by reversing the transformation that we did on the second article of this series:
  17. That implementation requires including the math declaration:
  18. We use the transformation after reading the value:
  19. And we use the public method in the main function. We should use the producer-consumer pattern here again to know when the proxy to the temperature characteristic is ready, but I have cut corners again for this initial implementation using a couple of delays to ensure that everything works fine.
  20. In order for this to work, the thread header must be included:
  21. We build and run to check that a value can be read.

Disconnect from the BLE sensor

Finally, we should disconnect from this device to leave things as we found them. If we don't, re-running the program won't work because the sensor will still be connected and busy.
  1. We declare a public method in the header to handle disconnections:
  2. And a private one to send the corresponding DBus message:
  3. In the implementation, the private method sends the required message and creates a closure that gets invoked when the device gets disconnected:
  4. And that closure has to change the connected flag using exclusive access:
  5. The private method is used from the public method:
  6. And the public method is used from the main function:
  7. Build and run to see the final result.

Recap and future work

In this article, I have used C++ to write an application that reads data from a Bluetooth Low Energy sensor. I have realized that writing C++ is not like riding a bike. Many things have changed since I wrote my last C++ code that went into production, but I hope I did a decent job at using it for this task. Frustration The biggest challenge wasn't the language, though. I banged my head against a brick wall every time I tried to figure out why I got "org.bluez.Error.Failed," caused by a "Connection Failed to be Established (0x3e)," when attempting to connect to the Bluetooth sensor. It happened often but not always. In the beginning, I didn't know if it was my code to blame, the library, or what. After catching exceptions everywhere, printing every message, capturing Bluetooth traces with btmon, and not finding much (although I did learn a few new things from Unix & Linux StackExchange, Stack Overflow and the Raspberry Pi forums), I suddenly realized that the culprit was the Raspberry Pi WiFi/Bluetooth chip. The symptom was an unreliable Bluetooth connection, but my sensor and the RPi were very close to each other and without any relevant interference from the environment. The root cause was sharing the radio frequency (RF) in the same chip (Broadcom BCM43438) with a relatively small antenna. I switched from the RPi3A+ to an RPi4B with an ethernet cable and WiFi disabled and, all of a sudden, things started to work.
Even though the implementation wasn't too complex and the proof of concept was passed, the hardware issue raised some concerns. It would only get worse if I talked to several sensors instead of just one. And that is exactly what we will do in future episodes to collect the data from the sensor and send it to a MongoDB Cluster with time series. I could still use a USB Bluetooth dongle and ignore the internal hardware. But before I take that road, I would like to work on the MQTT alternative and make a better informed decision. And that will be our next episode.
Stay curious, hack your code, and see you next time!

Facebook Icontwitter iconlinkedin icon
Rate this tutorial
star-empty
star-empty
star-empty
star-empty
star-empty

This is part of a series

Adventures in IoT with MongoDB
More in this series
Related
Tutorial

Turn BLE: Implementing BLE Sensors with MCU Devkits


Apr 02, 2024 | 13 min read
Tutorial

MongoDB Time Series with C++


Apr 03, 2024 | 6 min read
Tutorial

Plans and Hardware Selection for a Hands-on Implementation of IoT with MCUs and MongoDB


Nov 27, 2023 | 11 min read
Tutorial

Red Mosquitto: Implement a noise sensor with an MQTT client in an ESP32


Mar 18, 2024 | 25 min read
Table of Contents