Explore Developer Center's New Chatbot! MongoDB AI Chatbot can be accessed at the top of your navigation to answer all your MongoDB questions.

Join us at AWS re:Invent 2024! Learn how to use MongoDB for AI use cases.
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 Sep 17, 2024 • Updated Sep 17, 2024
RaspberryPiC++
FULL APPLICATION
Facebook Icontwitter iconlinkedin icon
Rate this tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
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.
1sudo apt-get install git git-flow cmake
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.
1sudo apt-get install libsdbus-c++-{bin,dev,doc}

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:
1#include <iostream>
2
3int main(int argc, char *argv[])
4{
5 std::cout << "Noise Collector BLE" << std::endl;
6
7 return 0;
8}

Basic project

And now we should create a CMakeLists.txt file with the minimal build instructions for this project:
1cmake_minimum_required(VERSION 3.5)
2project(NoiseCollectorBLE CXX)
3add_executable(${PROJECT_NAME} main.cpp)
Before we move forward, we are going to check that it all works fine:
1mkdir build
2cmake -S . -B build
3cmake --build build
4./build/NoiseCollectorBLE

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:
    1find_package(sdbus-c++ REQUIRED)
  2. The library must be linked to our binary:
    1target_link_libraries(${PROJECT_NAME} PRIVATE SDBusCpp::sdbus-c++)
  3. And we enforce the usage of the C++17 standard because it is required by the library:
    1set(CMAKE_CXX_STANDARD 17)
    2set(CMAKE_CXX_STANDARD_REQUIRED ON)
  4. With the library in place, let's create the skeleton to implement our BLE sensor. We first create the BleSensor.h file:
    1#ifndef BLE_SENSOR_H
    2#define BLE_SENSOR_H
    3
    4class BleSensor
    5{
    6};
    7
    8#endif // BLE_SENSOR_H
  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:
    1public:
    2 BleSensor();
    3 void scanAndConnect();
  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.
    1private:
    2 std::unique_ptr<sdbus::IProxy> bluezProxy;
  7. And we need to include the library:
    1#include <sdbus-c++/sdbus-c++.h>
  8. Let's create a BleSensor.cpp file for the implementation and include the header file that we have just created:
    1#include "BleSensor.h"
  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:
    1BleSensor::BleSensor()
    2{
    3 const std::string SERVICE_BLUEZ { "org.bluez" };
    4 const std::string OBJECT_PATH { "/org/bluez/hci0" };
    5
    6 bluezProxy = sdbus::createProxy(SERVICE_BLUEZ, OBJECT_PATH);
    7}
  10. Let's add the first step to our scanAndConnect method using a private function that we declare in the header:
    1bool getBluetoothStatus();
  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():
    1bool BleSensor::getBluetoothStatus()
    2{
    3 const std::string METHOD_GET { "Get" };
    4 const std::string INTERFACE_PROPERTIES { "org.freedesktop.DBus.Properties" };
    5 const std::string INTERFACE_ADAPTER { "org.bluez.Adapter1" };
    6 const std::string PROPERTY_POWERED { "Powered" };
    7 sdbus::Variant variant;
    8
    9 // Invoke a method that gets a property as a variant
    10 bluezProxy->callMethod(METHOD_GET)
    11 .onInterface(INTERFACE_PROPERTIES)
    12 .withArguments(INTERFACE_ADAPTER, PROPERTY_POWERED)
    13 .storeResultsTo(variant);
    14
    15 return (bool)variant;
    16}
  12. We use this private method from our public one:
    1void BleSensor::scanAndConnect()
    2{
    3 try
    4 {
    5 // Enable Bluetooth if not yet enabled
    6 if (getBluetoothStatus())
    7 {
    8 std::cout << "Bluetooth powered ON\n";
    9 } else
    10 {
    11 std::cout << "Powering bluetooth ON\n";
    12 }
    13 }
    14 catch(sdbus::Error& error)
    15 {
    16 std::cerr << "ERR: on scanAndConnect(): " << error.getName() << " with message " << error.getMessage() << std::endl;
    17 }
    18}
  13. And include the iostream header:
    1#include <iostream>
  14. We need to add the source files to the project:
    1file(GLOB SOURCES "*.cpp")
    2add_executable(${PROJECT_NAME} ${SOURCES})
  15. Finally, we import the header that we have defined in the main.cpp, create an instance of the object, and invoke the method:
    1#include "BleSensor.h"
    2
    3int main(int argc, char *argv[])
    4{
    5 std::cout << "Noise Collector BLE" << std::endl;
    6 BleSensor bleSensor;
    7 bleSensor.scanAndConnect();
  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:
    1void setBluetoothStatus(bool enable);
  2. And we also add it to the implementation file –in this case, only the message without the constants:
    1void BleSensor::setBluetoothStatus(bool enable)
    2{
    3 // Invoke a method that sets a property as a variant
    4 bluezProxy->callMethod(METHOD_SET)
    5 .onInterface(INTERFACE_PROPERTIES)
    6 .withArguments(INTERFACE_ADAPTER, PROPERTY_POWERED, sdbus::Variant(enable))
    7 // .dontExpectReply();
    8 .storeResultsTo();
    9}
  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:
    1const std::string METHOD_SET { "Set" };
  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:
    1static const std::string INTERFACE_ADAPTER { "org.bluez.Adapter1" };
    2static const std::string PROPERTY_POWERED { "Powered" };
    3static const std::string INTERFACE_PROPERTIES { "org.freedesktop.DBus.Properties" };
  5. With the private method complete, we use it from the public one:
    1if (getBluetoothStatus())
    2{
    3 std::cout << "Bluetooth powered ON\n";
    4} else
    5{
    6 std::cout << "Powering bluetooth ON\n";
    7 setBluetoothStatus(true);
    8}
  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:
    1void enableScanning(bool enable);
  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:
    1void BleSensor::enableScanning(bool enable)
    2{
    3 const std::string METHOD_START_DISCOVERY { "StartDiscovery" };
    4 const std::string METHOD_STOP_DISCOVERY { "StopDiscovery" };
    5
    6 std::cout << (enable?"Start":"Stop") << " scanning\n";
    7 bluezProxy->callMethod(enable?METHOD_START_DISCOVERY:METHOD_STOP_DISCOVERY)
    8 .onInterface(INTERFACE_ADAPTER)
    9 .dontExpectReply();
    10}
  3. We can then use that method in our public one to enable and disable scanning:
    1enableScanning(true);
    2// Wait to be connected to the sensor
    3enableScanning(false);
  4. We need to wait for the devices to answer, so let's add some delay between both calls:
    1// Wait to be connected to the sensor
    2std::this_thread::sleep_for(std::chrono::seconds(10))
  5. And we add the headers for this new code:
    1#include <chrono>
    2#include <thread>
  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:
    1std::unique_ptr<sdbus::IProxy> rootProxy;
  2. And instantiate it in the constructor:
    1rootProxy = sdbus::createProxy(SERVICE_BLUEZ, "/");
  3. Next, we define the private method that will take care of the subscription:
    1void subscribeToInterfacesAdded();
  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:
    1void BleSensor::subscribeToInterfacesAdded()
    2{
    3 const std::string INTERFACE_OBJ_MGR { "org.freedesktop.DBus.ObjectManager" };
    4 const std::string MEMBER_IFACE_ADDED { "InterfacesAdded" };
    5
    6 // Let's subscribe for the interfaces added signals (AddMatch)
    7 rootProxy->uponSignal(MEMBER_IFACE_ADDED).onInterface(INTERFACE_OBJ_MGR).call(interfaceAddedCallback);
    8 rootProxy->finishRegistration();
    9}
  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:
    1auto interfaceAddedCallback = [this](sdbus::ObjectPath path,
    2 std::map<std::string,
    3 std::map<std::string, sdbus::Variant>> dictionary)
    4{
    5};
  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:
    1const std::regex DEVICE_INSTANCE_RE{"^/org/bluez/hci[0-9]/dev(_[0-9A-F]{2}){6}$"};
    2std::smatch match;
    3std::cout << "(TID: " << std::this_thread::get_id() << ") ";
    4if (std::regex_match(path, match, DEVICE_INSTANCE_RE)) {
    5 std::cout << "Device iface ";
    6
    7 if (dictionary["org.bluez.Device1"].count("Name") == 1)
    8 {
    9 auto name = (std::string)(dictionary["org.bluez.Device1"].at("Name"));
    10 std::cout << name << " @ " << path << std::endl;
    11 } else
    12 {
    13 std::cout << "<NAMELESS> @ " << path << std::endl;
    14 }
    15} else {
    16 std::cout << "*** UNEXPECTED SIGNAL ***";
    17}
  7. And we add the header for regular expressions:
    1#include <regex>
  8. We use the private method before we start scanning:
    1subscribeToInterfacesAdded();
  9. And we print the thread id in that same method:
    1std::cout << "(TID: " << std::this_thread::get_id() << ") ";
  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:
    1void connectToDevice(sdbus::ObjectPath path);
  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:
    1BleSensor(const std::string &sensor_name);
  3. And declare a field that will be used to hold the value:
    1const std::string deviceName;
  4. If we find the device, we will create a proxy to the object that represents it:
    1std::unique_ptr<sdbus::IProxy> deviceProxy;
  5. We move to the implementation and start by adapting the constructor to initialize the new values using the preamble:
    1BleSensor::BleSensor(const std::string &sensor_name)
    2 : deviceProxy{nullptr}, deviceName{sensor_name}
  6. We then create the method:
    1void BleSensor::connectToDevice(sdbus::ObjectPath path)
    2{
    3}
  7. We create a proxy for the device that we have selected using the name:
    1deviceProxy = sdbus::createProxy(SERVICE_BLUEZ, path);
  8. And move the declaration of the service constant, which is now used in two places, to the header:
    1inline static const std::string SERVICE_BLUEZ{"org.bluez"};
  9. And send a message to connect to it:
    1deviceProxy->callMethodAsync(METHOD_CONNECT).onInterface(INTERFACE_DEVICE).uponReplyInvoke(connectionCallback);
    2std::cout << "Connection method started" << std::endl;
  10. We define the constants that we are using:
    1const std::string INTERFACE_DEVICE{"org.bluez.Device1"};
    2const std::string METHOD_CONNECT{"Connect"};
  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.
    1auto connectionCallback = [this](const sdbus::Error *error)
    2{
    3};
  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:
    1if (name == deviceName)
    2{
    3 std::cout << "Connecting to " << name << std::endl;
    4 connectToDevice(path);
    5}
  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:
    1std::mutex mtx;
    2std::condition_variable cv;
    3bool connected;
  14. And we include the required headers:
    1#include <condition_variable>
  15. They are initialized in the constructor preamble:
    1BleSensor::BleSensor(const std::string &sensor_name)
    2 : deviceProxy{nullptr}, deviceName{sensor_name},
    3 cv{}, mtx{}, connected{false}
  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:
    1std::unique_lock<std::mutex> lock(mtx);
  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:
    1enableScanning(true);
    2// Wait to be connected to the sensor
    3cv.wait(lock, [this]()
    4 { return connected; });
    5enableScanning(false);
  18. In the connectionCallback, we first deal with errors, in case they happen:
    1if (error != nullptr)
    2{
    3 std::cerr << "Got connection error "
    4 << error->getName() << " with message "
    5 << error->getMessage() << std::endl;
    6 return;
    7}
  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:
    1std::unique_lock<std::mutex> lock(mtx);
    2std::cout << "Connected!!!" << std::endl;
    3connected = true;
    4lock.unlock();
    5cv.notify_one();
    6std::cout << "Finished connection method call" << std::endl;
  20. Finally, we change the initialization of the BleSensor in the main file to pass the sensor name:
    1BleSensor bleSensor { "RP2-SENSOR" };
  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:
    1if (!connected)
    2{
    3 // Current code with regex goes here.
    4}
    5else
    6{
    7}
  2. In the else part, we add a different match:
    1if (std::regex_match(path, match, DEVICE_ATTRS_RE))
    2{
    3}
    4else
    5{
    6 std::cout << "Not a characteristic" << std::endl;
    7}
  3. That code requires the regular expression declared in the method:
    1const std::regex DEVICE_ATTRS_RE{"^/org/bluez/hci\\d/dev(_[0-9A-F]{2}){6}/service\\d{4}/char\\d{4}"};
  4. If the path matches the expression, we check if it has the UUID of the characteristic that we want to read:
    1std::cout << "Characteristic " << path << std::endl;
    2if ((dictionary.count("org.bluez.GattCharacteristic1") == 1) &&
    3 (dictionary["org.bluez.GattCharacteristic1"].count("UUID") == 1))
    4{
    5 auto name = (std::string)(dictionary["org.bluez.GattCharacteristic1"].at("UUID"));
    6 if (name == "00002a1c-0000-1000-8000-00805f9b34fb")
    7 {
    8 }
    9}
  5. When we find the desired characteristic, we need to create (yes, you guessed it) a proxy to send messages to it.
    1tempAttrProxy = sdbus::createProxy(SERVICE_BLUEZ, path);
    2std::cout << "<<<FOUND>>> " << path << std::endl;
  6. That proxy is stored in a field that we haven't declared yet. Let's do so in the header file:
    1std::unique_ptr<sdbus::IProxy> tempAttrProxy;
  7. And we do an explicit initialization in the constructor preamble:
    1BleSensor::BleSensor(const std::string &sensor_name)
    2 : deviceProxy{nullptr}, tempAttrProxy{nullptr},
    3 cv{}, mtx{}, connected{false}, deviceName{sensor_name}
  8. Everything is ready to read, so let's declare a public method to do the reading:
    1void getValue();
  9. And a private method to send the DBus messages:
    1void readTemperature();
  10. We implement the public method, just using the private method:
    1void BleSensor::getValue()
    2{
    3 readTemperature();
    4}
  11. And we do the implementation on the private method:
    1void BleSensor::readTemperature()
    2{
    3 tempAttrProxy->callMethod(METHOD_READ)
    4 .onInterface(INTERFACE_CHAR)
    5 .withArguments(args)
    6 .storeResultsTo(result);
    7}
  12. We define the constants that we used:
    1const std::string INTERFACE_CHAR{"org.bluez.GattCharacteristic1"};
    2const std::string METHOD_READ{"ReadValue"};
  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:
    1std::map<std::string, sdbus::Variant> args{{{"offset", sdbus::Variant{std::uint16_t{0}}}}};
    2std::vector<std::uint8_t> result;
  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:
    1std::cout << "READ: ";
    2for (auto value : result)
    3{
    4 std::cout << +value << " ";
    5}
    6std::vector number(result.begin() + 1, result.end());
  15. Those bytes in ieee11073 format have to be transformed into a regular float, and we use a private method for that:
    1float valueFromIeee11073(std::vector<std::uint8_t> binary);
  16. That method is implemented by reversing the transformation that we did on the second article of this series:
    1float BleSensor::valueFromIeee11073(std::vector<std::uint8_t> binary)
    2{
    3 float value = static_cast<float>(binary[0]) + static_cast<float>(binary[1]) * 256.f + static_cast<float>(binary[2]) * 256.f * 256.f;
    4 float exponent;
    5 if (binary[3] > 127)
    6 {
    7 exponent = static_cast<float>(binary[3]) - 256.f;
    8 }
    9 else
    10 {
    11 exponent = static_cast<float>(binary[3]);
    12 }
    13 return value * pow(10, exponent);
    14}
  17. That implementation requires including the math declaration:
    1#include <cmath>
  18. We use the transformation after reading the value:
    1std::cout << "\nTemp: " << valueFromIeee11073(number);
    2std::cout << std::endl;
  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.
    1std::this_thread::sleep_for(std::chrono::seconds(5));
    2bleSensor.getValue();
    3std::this_thread::sleep_for(std::chrono::seconds(5));
  20. In order for this to work, the thread header must be included:
    1#include <thread>
  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:
    1void disconnect();
  2. And a private one to send the corresponding DBus message:
    1void disconnectFromDevice();
  3. In the implementation, the private method sends the required message and creates a closure that gets invoked when the device gets disconnected:
    1void BleSensor::disconnectFromDevice()
    2{
    3 const std::string INTERFACE_DEVICE{"org.bluez.Device1"};
    4 const std::string METHOD_DISCONNECT{"Disconnect"};
    5
    6 auto disconnectionCallback = [this](const sdbus::Error *error)
    7 {
    8 };
    9
    10 {
    11 deviceProxy->callMethodAsync(METHOD_DISCONNECT).onInterface(INTERFACE_DEVICE).uponReplyInvoke(disconnectionCallback);
    12 std::cout << "Disconnection method started" << std::endl;
    13 }
    14}
  4. And that closure has to change the connected flag using exclusive access:
    1if (error != nullptr)
    2{
    3 std::cerr << "Got disconnection error " << error->getName() << " with message " << error->getMessage() << std::endl;
    4 return;
    5}
    6std::unique_lock<std::mutex> lock(mtx);
    7std::cout << "Disconnected!!!" << std::endl;
    8connected = false;
    9deviceProxy = nullptr;
    10lock.unlock();
    11std::cout << "Finished connection method call" << std::endl;
  5. The private method is used from the public method:
    1void BleSensor::disconnect()
    2{
    3 std::cout << "Disconnecting from device" << std::endl;
    4 disconnectFromDevice();
    5}
  6. And the public method is used from the main function:
    1bleSensor.disconnect();
  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!
Top Comments in Forums
Forum Commenter Avatar
Kevin_KempKevin Kemplast month

Is the full source code for this project available? I’m trying to build this from the article, but I’m getting lost with the CMakeLists.txt file. I know c++ well, but I’m not so good with cmake.

BTW, the project I’m making isn’t exactly like yours, but it is close, I’m using a raspberry pi model 4B for the central and Arduino nano 33 iot for the peripherals. I plan to have around 12 peripherals. I have 4 of them now and they are working. I just need to create a central that can connect to them. BTW, this is my first BLE project.


Forum Commenter Avatar
Jorge_Ortiz_FuentesJorge Ortiz Fuenteslast month

Hi Kevin,
The code is here. I hope that solves your question, but feel free to ask if it doesn’t.
Best of luck with your project.
Best regards,

Jorge

See More on Forums

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

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


Aug 28, 2024 | 11 min read
Tutorial

CMake + Conan + VS Code


Sep 04, 2024 | 6 min read
Tutorial

Red Mosquitto: Implement a Noise Sensor With an MQTT Client in an ESP32


Sep 17, 2024 | 25 min read
Tutorial

Me and the Devil BlueZ: Implementing a BLE Central in Linux - Part 1


Dec 14, 2023 | 10 min read
Table of Contents