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

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: Implementing a BLE Central in Linux - Part 1

Jorge D. Ortiz-Fuentes10 min read • Published Dec 12, 2023 • Updated Dec 14, 2023
RaspberryPiC++
SNIPPET
Facebook Icontwitter iconlinkedin icon
Rate this tutorial
star-empty
star-empty
star-empty
star-empty
star-empty
In my last article, I covered the basic Bluetooth Low Energy concepts required to implement a BLE peripheral in an MCU board. We used a Raspberry Pi Pico board and MicroPython for our implementation. We ended up with a prototype firmware that used the on-board LED, read from the on-board temperature sensor, and implemented a BLE peripheral with two services and several characteristics – one that depended on measured data and could push notifications to its client.
In this article, we will be focusing on the other side of the BLE communication: the BLE central, rather than the BLE peripheral. Our collecting station is going to gather the data from the sensors and it is a Raspberry Pi 3A+ with a Linux distribution, namely, Raspberry Pi OS wormbook which is a Debian derivative commonly used in this platform. Debian

Concepts

Different platforms use different libraries to interact with Bluetooth hardware when it is available. In the case of Linux, BlueZ became the official Bluetooth stack in 2004, replacing the previously available OpenBT.
Initially, all the tools were command-line based and the libraries used raw sockets to access the Host Controller Interface offered by hardware. But since the early beginning of its adoption, there was interest to integrate it into the different desktop alternatives, mainly Gnome and KDE. Sharing the Bluetooth interface across the different desktop applications required a different approach: a daemon that took care of all the Bluetooth tasks that take place outside of the Linux Kernel, and an interface that would allow sharing access to that daemon. D-Bus had been designed as a common initiative for interoperability among free-software desktop environments, managed by FreeDesktop, and had already been adopted by the major Linux desktops, so it became the preferred option for that interface.

D-Bus

D-Bus, short for desktop bus, is an interprocess communication mechanism that uses a message bus. The bus is responsible for taking the messages sent by any process connected to it and delivering them to other processes in the same bus. A fast bus
There are two types of message bus. There is the system bus that permits connecting with the different system components, like the Bluetooth stack, that is controlled via BlueZ. There is also a session bus for each user logged into the system.
In order to use Bluetooth from an application, the application needs to connect to the system message bus and interact with it. Services get connected to the D-Bus by registering to it. D-Bus keeps an inventory of these things, these pieces of functionality that are connected to the bus. They are represented as objects, in the sense of object-oriented design. Each available object implements one or more interfaces which are represented with reverse DNS strings. Interfaces have properties, methods, and signals. Properties have values that can be get or set. Methods can be invoked and may or may not have a result. But interfaces also define signals (i.e., events) that an object can emit without any external trigger.
When we connect to D-Bus as a client, it provides us with a unique connection name (unique identifier of this connection). When objects are exported to the D-Bus –i.e., registered to it– they get a unique identifier. That identifier is encoded as a path which is used to route and deliver messages to that object. Applications may send messages to those objects and/or subscribe to signals emitted by them.

Using command-line tools

Linux, among other operating systems, implements a thin layer to enable communication between the host and the controller of the Bluetooth stack. That layer is known as Host-Controller Interface.
Enabling the Bluetooth radio was usually done with sudo hciconfig hci0 up. Nowadays, we can use bluetoothctl instead:
1bluetoothctl
2[bluetooth]# show
3Controller XX:XX:XX:XX:XX:XX (public)
4 Name: ...
5 Alias: ...
6 Powered: no
7 ...
8[bluetooth]# power on
9Changing power on succeeded
10[CHG] Controller XX:XX:XX:XX:XX:XX Powered: yes
11[bluetooth]# show
12Controller XX:XX:XX:XX:XX:XX (public)
13 Name: ...
14 Alias: ...
15 Powered: yes
16 ...
With the radio on, we can start scanning for BLE devices:
1bluetoothctl
2[bluetooth]# menu scan
3[bluetooth]# transport le
4[bluetooth]# back
5[bluetooth]# scan on
6[bluetooth]# devices
This shows several devices and my RP2 here:
Device XX:XX:XX:XX:XX RP2-SENSOR
Now that we know the MAC address/name pairs, we can use the former piece of data to connect to it:
1 [bluetooth]# connect XX:XX:XX:XX:XX:XX
2 Attempting to connect to XX:XX:XX:XX:XX:XX
3 Connection successful
4 [NEW] Primary Service (Handle 0x2224)
5 /org/bluez/hci0/dev_28_CD_C1_0F_4B_AE/service0004
6 00001801-0000-1000-8000-00805f9b34fb
7 Generic Attribute Profile
8[NEW] Characteristic (Handle 0x7558)
9 /org/bluez/hci0/dev_28_CD_C1_0F_4B_AE/service0004/char0005
10 00002a05-0000-1000-8000-00805f9b34fb
11 Service Changed
12[NEW] Primary Service (Handle 0x78c4)
13 /org/bluez/hci0/dev_28_CD_C1_0F_4B_AE/service0007
14 0000180a-0000-1000-8000-00805f9b34fb
15 Device Information
16[NEW] Characteristic (Handle 0x7558)
17 /org/bluez/hci0/dev_28_CD_C1_0F_4B_AE/service0007/char0008
18 00002a29-0000-1000-8000-00805f9b34fb
19 Manufacturer Name String
20[NEW] Characteristic (Handle 0x7558)
21 /org/bluez/hci0/dev_28_CD_C1_0F_4B_AE/service0007/char000a
22 00002a24-0000-1000-8000-00805f9b34fb
23 Model Number String
24[NEW] Characteristic (Handle 0x7558)
25 /org/bluez/hci0/dev_28_CD_C1_0F_4B_AE/service0007/char000c
26 00002a25-0000-1000-8000-00805f9b34fb
27 Serial Number String
28[NEW] Characteristic (Handle 0x7558)
29 /org/bluez/hci0/dev_28_CD_C1_0F_4B_AE/service0007/char000e
30 00002a26-0000-1000-8000-00805f9b34fb
31 Firmware Revision String
32[NEW] Characteristic (Handle 0x7558)
33 /org/bluez/hci0/dev_28_CD_C1_0F_4B_AE/service0007/char0010
34 00002a27-0000-1000-8000-00805f9b34fb
35 Hardware Revision String
36[NEW] Primary Service (Handle 0xb324)
37 /org/bluez/hci0/dev_28_CD_C1_0F_4B_AE/service0012
38 0000181a-0000-1000-8000-00805f9b34fb
39 Environmental Sensing
40[NEW] Characteristic (Handle 0x7558)
41 /org/bluez/hci0/dev_28_CD_C1_0F_4B_AE/service0012/char0013
42 00002a1c-0000-1000-8000-00805f9b34fb
43 Temperature Measurement
44[NEW] Descriptor (Handle 0x75a0)
45 /org/bluez/hci0/dev_28_CD_C1_0F_4B_AE/service0012/char0013/desc0015
46 00002902-0000-1000-8000-00805f9b34fb
47 Client Characteristic Configuration
48[NEW] Descriptor (Handle 0x75a0)
49 /org/bluez/hci0/dev_28_CD_C1_0F_4B_AE/service0012/char0013/desc0016
50 0000290d-0000-1000-8000-00805f9b34fb
51 Environmental Sensing Trigger Setting
52[RP2-SENSOR]# scan off
Now we can use the General Attribute Profile (GATT) to send commands to the device, including listing the attributes, reading a characteristic, and receiving notifications.
1[RP2-SENSOR]# menu gatt
2[RP2-SENSOR]# list-attributes
3...
4Characteristic (Handle 0x0001)
5 /org/bluez/hci0/dev_28_CD_C1_0F_4B_AE/service0012/char0013
6 00002a1c-0000-1000-8000-00805f9b34fb
7 Temperature Measurement
8...
9[RP2-SENSOR]# select-attribute /org/bluez/hci0/dev_28_CD_C1_0F_4B_AE/service0012/char0013
10[MPY BTSTACK:/service0012/char0013]# read
11Attempting to read /org/bluez/hci0/dev_28_CD_C1_0F_4B_AE/service0012/char0013
12[CHG] Attribute /org/bluez/hci0/dev_28_CD_C1_0F_4B_AE/service0012/char0013 Value:
13 00 0c 10 00 fe .....
14 00 0c 10 00 fe .....
15[MPY BTSTACK:/service0012/char0013]# notify on
16[CHG] Attribute /org/bluez/hci0/dev_28_CD_C1_0F_4B_AE/service0012/char0013 Notifying: yes
17Notify started
18[CHG] Attribute /org/bluez/hci0/dev_28_CD_C1_0F_4B_AE/service0012/char0013 Value:
19 00 3b 10 00 fe .;...
20[CHG] Attribute /org/bluez/hci0/dev_28_CD_C1_0F_4B_AE/service0012/char0013 Value:
21 00 6a 10 00 fe .j...
22[MPY BTSTACK:/service0012/char0013]# notify off
And we leave it in its original state:
1[MPY BTSTACK:/service0012/char0013]# back
2[MPY BTSTACK:/service0012/char0013]# disconnect
3Attempting to disconnect from 28:CD:C1:0F:4B:AE
4[CHG] Device 28:CD:C1:0F:4B:AE ServicesResolved: no
5Successful disconnected
6[CHG] Device 28:CD:C1:0F:4B:AE Connected: no
7[bluetooth]# power off
8Changing power off succeeded
9[CHG] Controller B8:27:EB:4D:70:A6 Powered: no
10[CHG] Controller B8:27:EB:4D:70:A6 Discovering: no
11[bluetooth]# exit

Query the services in the system bus

dbus-send comes with D-Bus.
We are going to send a message to the system bus. The message is addressed to "org.freedesktop.DBus" which is the service implemented by D-Bus itself. We use the single D-Bus instance, "/org/freedesktop/DBus". And we use the "Introspect" method of the "org.freedesktop.DBus.Introspectable". Hence, it is a method call. Finally, it is important to highlight that we must request that the reply gets printed, with "–print-reply" if we want to be able to watch it.
1dbus-send --system --print-reply --dest=org.freedesktop.DBus /org/freedesktop/DBus org.freedesktop.DBus.Introspectable.Introspect | less
This method call has a long reply, but let me highlight some interesting parts. Right after the header, we get the description of the interface "org.freedesktop.DBus":
1<node>
2 <interface name="org.freedesktop.DBus">
3 <method name="Hello">
4 <arg direction="out" type="s"/>
5 </method>
6 <method name="RequestName">
7 ...
8 </method>
9 <method name="ReleaseName">
10 ...
These are the methods, properties and signals related to handling connections to the bus and information about it. Methods may have parameters (args with direction "in") and results (args with direction "out") and both define the type of the expected data. Signals also declare the arguments, but they are broadcasted and no response is expected, so there is no need to use "direction."
Then we have an interface to expose the D-Bus properties:
1<interface name="org.freedesktop.DBus.Properties">
2...
And a description of the "org.freedesktop.DBus.Introspectable" interface that we have already used to obtain all the interfaces. Inception? Maybe.
1<interface name="org.freedesktop.DBus.Introspectable">
2 <method name="Introspect">
3 <arg direction="out" type="s"/>
4 </method>
5</interface>
Finally, we find three other interfaces:
1 <interface name="org.freedesktop.DBus.Monitoring">
2 ...
3 </interface>
4 <interface name="org.freedesktop.DBus.Debug.Stats">
5 ...
6 </interface>
7 <interface name="org.freedesktop.DBus.Peer">
8 ...
9 </interface>
10</node>
Let's use the method of the first interface that tells us what is connected to the bus. In my case, I get:
1dbus-send --system --print-reply --dest=org.freedesktop.DBus /org/freedesktop/DBus org.freedesktop.DBus.ListNames
2method return time=1698320750.822056 sender=org.freedesktop.DBus -> destination=:1.50 serial=3 reply_serial=2
3 array [
4 string "org.freedesktop.DBus"
5 string ":1.7"
6 string "org.freedesktop.login1"
7 string "org.freedesktop.timesync1"
8 string ":1.50"
9 string "org.freedesktop.systemd1"
10 string "org.freedesktop.Avahi"
11 string "org.freedesktop.PolicyKit1"
12 string ":1.43"
13 string "org.bluez"
14 string "org.freedesktop.ModemManager1"
15 string ":1.0"
16 string ":1.1"
17 string ":1.2"
18 string ":1.3"
19 string ":1.4"
20 string "fi.w1.wpa_supplicant1"
21 string ":1.5"
22 string ":1.6"
23 ]
The "org.bluez" is the service that we want to use. We can use introspect with it:
1dbus-send --system --print-reply=literal --dest=org.bluez /org/bluez org.freedesktop.DBus.Introspectable.Introspect |
2xmllint --format - | less
xmllint can be installed with sudo apt-get install libxml2-utils.
After the header, I get the following interfaces:
1<node>
2 <interface name="org.freedesktop.DBus.Introspectable">
3 <method name="Introspect">
4 <arg name="xml" type="s" direction="out"/>
5 </method>
6 </interface>
7 <interface name="org.bluez.AgentManager1">
8 <method name="RegisterAgent">
9 <arg name="agent" type="o" direction="in"/>
10 <arg name="capability" type="s" direction="in"/>
11 </method>
12 <method name="UnregisterAgent">
13 <arg name="agent" type="o" direction="in"/>
14 </method>
15 <method name="RequestDefaultAgent">
16 <arg name="agent" type="o" direction="in"/>
17 </method>
18 </interface>
19 <interface name="org.bluez.ProfileManager1">
20 <method name="RegisterProfile">
21 <arg name="profile" type="o" direction="in"/>
22 <arg name="UUID" type="s" direction="in"/>
23 <arg name="options" type="a{sv}" direction="in"/>
24 </method>
25 <method name="UnregisterProfile">
26 <arg name="profile" type="o" direction="in"/>
27 </method>
28 </interface>
29 <interface name="org.bluez.HealthManager1">
30 <method name="CreateApplication">
31 <arg name="config" type="a{sv}" direction="in"/>
32 <arg name="application" type="o" direction="out"/>
33 </method>
34 <method name="DestroyApplication">
35 <arg name="application" type="o" direction="in"/>
36 </method>
37 </interface>
38 <node name="hci0"/>
39</node>
Have you noticed the node that represents the child object for the HCI0? We could also have learned about it using busctl tree org.bluez. And we can query that child object too. We will now obtain the information about HCI0 using introspection but send the message to BlueZ and refer to the HCI0 instance.
1dbus-send --system --print-reply=literal --dest=org.bluez /org/bluez/hci0 org.freedesktop.DBus.Introspectable.Introspect | xmllint --format - | less
1<node>
2 <interface name="org.freedesktop.DBus.Introspectable">
3 <method name="Introspect">
4 <arg name="xml" type="s" direction="out"/>
5 </method>
6 </interface>
7 <interface name="org.bluez.Adapter1">
8 <method name="StartDiscovery"/>
9 <method name="SetDiscoveryFilter">
10 <arg name="properties" type="a{sv}" direction="in"/>
11 </method>
12 <method name="StopDiscovery"/>
13 <method name="RemoveDevice">
14 <arg name="device" type="o" direction="in"/>
15 </method>
16 <method name="GetDiscoveryFilters">
17 <arg name="filters" type="as" direction="out"/>
18 </method>
19 <property name="Address" type="s" access="read"/>
20 <property name="AddressType" type="s" access="read"/>
21 <property name="Name" type="s" access="read"/>
22 <property name="Alias" type="s" access="readwrite"/>
23 <property name="Class" type="u" access="read"/>
24 <property name="Powered" type="b" access="readwrite"/>
25 <property name="Discoverable" type="b" access="readwrite"/>
26 <property name="DiscoverableTimeout" type="u" access="readwrite"/>
27 <property name="Pairable" type="b" access="readwrite"/>
28 <property name="PairableTimeout" type="u" access="readwrite"/>
29 <property name="Discovering" type="b" access="read"/>
30 <property name="UUIDs" type="as" access="read"/>
31 <property name="Modalias" type="s" access="read"/>
32 <property name="Roles" type="as" access="read"/>
33 </interface>
34 <interface name="org.freedesktop.DBus.Properties">
35 <method name="Get">
36 <arg name="interface" type="s" direction="in"/>
37 <arg name="name" type="s" direction="in"/>
38 <arg name="value" type="v" direction="out"/>
39 </method>
40 <method name="Set">
41 <arg name="interface" type="s" direction="in"/>
42 <arg name="name" type="s" direction="in"/>
43 <arg name="value" type="v" direction="in"/>
44 </method>
45 <method name="GetAll">
46 <arg name="interface" type="s" direction="in"/>
47 <arg name="properties" type="a{sv}" direction="out"/>
48 </method>
49 <signal name="PropertiesChanged">
50 <arg name="interface" type="s"/>
51 <arg name="changed_properties" type="a{sv}"/>
52 <arg name="invalidated_properties" type="as"/>
53 </signal>
54 </interface>
55 <interface name="org.bluez.GattManager1">
56 <method name="RegisterApplication">
57 <arg name="application" type="o" direction="in"/>
58 <arg name="options" type="a{sv}" direction="in"/>
59 </method>
60 <method name="UnregisterApplication">
61 <arg name="application" type="o" direction="in"/>
62 </method>
63 </interface>
64 <interface name="org.bluez.LEAdvertisingManager1">
65 <method name="RegisterAdvertisement">
66 <arg name="advertisement" type="o" direction="in"/>
67 <arg name="options" type="a{sv}" direction="in"/>
68 </method>
69 <method name="UnregisterAdvertisement">
70 <arg name="service" type="o" direction="in"/>
71 </method>
72 <property name="ActiveInstances" type="y" access="read"/>
73 <property name="SupportedInstances" type="y" access="read"/>
74 <property name="SupportedIncludes" type="as" access="read"/>
75 <property name="SupportedSecondaryChannels" type="as" access="read"/>
76 </interface>
77 <interface name="org.bluez.Media1">
78 <method name="RegisterEndpoint">
79 <arg name="endpoint" type="o" direction="in"/>
80 <arg name="properties" type="a{sv}" direction="in"/>
81 </method>
82 <method name="UnregisterEndpoint">
83 <arg name="endpoint" type="o" direction="in"/>
84 </method>
85 <method name="RegisterPlayer">
86 <arg name="player" type="o" direction="in"/>
87 <arg name="properties" type="a{sv}" direction="in"/>
88 </method>
89 <method name="UnregisterPlayer">
90 <arg name="player" type="o" direction="in"/>
91 </method>
92 <method name="RegisterApplication">
93 <arg name="application" type="o" direction="in"/>
94 <arg name="options" type="a{sv}" direction="in"/>
95 </method>
96 <method name="UnregisterApplication">
97 <arg name="application" type="o" direction="in"/>
98 </method>
99 </interface>
100 <interface name="org.bluez.NetworkServer1">
101 <method name="Register">
102 <arg name="uuid" type="s" direction="in"/>
103 <arg name="bridge" type="s" direction="in"/>
104 </method>
105 <method name="Unregister">
106 <arg name="uuid" type="s" direction="in"/>
107 </method>
108 </interface>
109</node>
Let's check the status of the Bluetooth radio using D-Bus messages to query the corresponding property:
1dbus-send --system --type=method_call --print-reply --dest=org.bluez /org/bluez/hci0 org.freedesktop.DBus.Properties.Get string:org.bluez.Adapter1 string:Powered
We can then switch the radio on, setting the same property:
1dbus-send --system --type=method_call --print-reply --dest=org.bluez /org/bluez/hci0 org.freedesktop.DBus.Properties.Set string:org.bluez.Adapter1 string:Powered variant:boolean:true
And check the status of the radio again to verify the change:
1dbus-send --system --type=method_call --print-reply --dest=org.bluez /org/bluez/hci0 org.freedesktop.DBus.Properties.Get string:org.bluez.Adapter1 string:Powered
The next step is to start scanning, and it seems that we should use this command:
1dbus-send --system --type=method_call --print-reply --dest=org.bluez /org/bluez/hci0 org.bluez.Adapter1.StartDiscovery
But this doesn't work because dbus-send exits almost immediately and BlueZ keeps track of the D-Bus clients that request the discovery.

Capture the messages produced by

bluetoothctl
Instead, we are going to use the command line utility bluetoothctl and monitor the messages that go through the system bus.
We start dbus-monitor for the system bus and redirect the output to a file. We launch bluetoothctl and inspect the log. This connects to the D-Bus with a "Hello" method. It invokes AddMatch to show interest in BlueZ. It does GetManagedObjects to find the objects that are managed by BlueZ.
We then select Low Energy (menu scan, transport le, back). This doesn't produce messages because it just configures the tool.
We start scanning (scan on), connect to the device (connect XX:XX:XX:XX:XX:XX), and stop scanning (scan off). In the log, the second message is a method call to start scanning (StartDiscovery), preceded by a call (to SetDiscoveryFilter) with LE as a parameter. Then, we find signals –one per device that is discoverable– with all the metadata of the device, including its MAC address, its name (if available), and the transmission power that is normally used to estimate how close a device is, among other properties. The app shows its interest in the devices it has found with an AddMatch method call, and we can see signals with properties updates.
Then, a call to the method Connect of the org.bluez.Device1 interface is invoked with the path pointing to the desired device. Finally, when we stop scanning, we can find an immediate call to StopDiscovery, and the app declares that it is no longer interested in updates of the previously discovered devices with calls to the RemoveMatch method. A little later, an announcement signal tells us that the "connected" property of that device has changed, and then there's a signal letting us know that InterfacesAdded implemented org.bluez.GattService1, org.bluez.GattCharacteristic1 for each of the services and characteristics. We get a signal with a "ServicesResolved" property stating that the present services are Generic Access Service, Generic Attribute Service, Device Information Service, and Environmental Sensing Service (0x1800, 0x1801, 0x180A, and 0x181A). In the process, the app uses AddMatch to show interest in the different services and characteristics.
We select the attribute for the temperature characteristic (select-attribute /org/bluez/hci0/dev_28_CD_C1_0F_4B_AE/service0012/char0013), which doesn't produce any D-Bus messages. Then, we read the characteristic that generates a method call to ReadValue of the org.bluez.GattCharacteristic1 interface with the path that we have previously selected. Right after, we receive a method return message with the five bytes of that characteristic.
As for notifications, when we enable them (notify on), a method call to StartNotify is issued with the same parameters as the ReadValue one. The notification comes as a PropertiesChanged signal that contains the new value and then we send the StopNotify command. Both changes to the notification state produce signals that share the new state.

Recap and future content

In this article, I have explained all the steps required to interact with the BLE peripheral from the command line. Then, I did some reverse engineering to understand how those steps translated into D-Bus messages. Find the resources for this article and links to others.
In the next article, I will try to use the information that we have gathered about the D-Bus messages to interact with the Bluetooth stack using C++.
If you have questions or feedback, join me in the MongoDB Developer Community!

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
Up Next
Continue

More in this series
Related
Tutorial

Me and the Devil BlueZ: Reading BLE Sensors From C++


Sep 17, 2024 | 16 min read
Tutorial

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


Aug 28, 2024 | 11 min read
Tutorial

Storing Binary Data with MongoDB and C++


Sep 18, 2023 | 6 min read
Tutorial

Turn BLE: Implementing BLE Sensors with MCU Devkits


Apr 02, 2024 | 13 min read
Table of Contents
  • Concepts