# Introduction This document will provide an introduction to Bluetooth and some information about the Bluetooth stack on linux and some tools that are useful. # Overview ## Bluetooth classic ### History Developed in the late 90’s and 1998 the Bluetooth Special Interest Group (Bluetooth SIG) was formed among a consortium of companies to push the standard. Bluetooth v2.0 solved a lot of issues and upped the maximum throughput to 3Mbps. In v2.1 one level of security were introduced by means of pairing devices. Bluetooth v3.0 introduced a way for paired devices to send data in higher speed via another medium (wifi or phy), this never got traction and is very seldom used. In Bluetooth v4.0 the Bluetooth low energy (Bluetooth LE) was introduced and now there were two different way of sending radio signals (devices could have one or the other or support for both techniques, dual mode). Bluetooth classic is sometimes referred to as BR/EDR which is the modulation schemes it supports. Bluetooth classic is mainly used for audio streaming. ### Technique Bluetooth classic uses the 2.4GHz ISM band. The band extends from 2400MHz to 2483.5MHz. This band is divided into 78 channels by Bluetooth classic, spaced 1MHz apart from 2402 to 2480MHz. By using an adaptive frequency hopping algorithm, the goal is to avoid interference from other devices using the same spectra as baby monitors, wi-fi and microwave ovens. ### Protocol stack ![Protocol stack in blocks](images/media/image1.png) Classic profiles There is a large set of Bluetooth profiles, the most common are. - **A2DP** - Advanced Audio Distribution Profile is a profile that allows streaming audio from a source to a sink. For example, when you stream music from your iPhone to your car, this is done using the A2DP profile. - **HFP** - Hands-Free Profile used in Bluetooth headsets - **SPP** - Serial Port Profile emulates serial ports over Bluetooth to provide a simple substitute for existing RS-232 - **PBAP** - Phone Book Access Profile allows access to a phone's Phone Book, for example to display in a car to allow dialing - **MAP** - Message Access Profile allows exchange of messages between devices, for example to read SMS from a phone - **AVRCP** - A/V Remote Control Profile allows a Bluetooth device to act as a remote control, for example controlling video playback - **PAN** – Personal Area Network Profile used for sharing networking ## Bluetooth Low Energy (Bluetooth LE) ### History Sometimes called Bluetooth Smart was introduced in Bluetooth 4.0. It started in a Nokia project (2001) called Wibree before being adopted by the Bluetooth SIG. ### Bluetooth LE Protocoll stack ![Bluetooth LE protocoll stack](images/media/image2.png) ### GAP The Generic Access Profile (GAP) controls connections and advertising in Bluetooth. GAP enables the visibility of devices and determines how devices can interact with each other. A device is either a peripheral or a central device. A peripheral device is often a small, low power resource constrained device that can connect to a much more powerful central device. Peripheral devices are typically things like sensors, proximity tags etc. Central devices are more powerful, usually a pc, tablet or mobile phone. ### Advertising and scan response data A peripheral device can send 31 bytes of data in two ways. The advertising data and the scan response data. Only the advertising data is mandatory. If the peripheral device supports ***scan response data*** the central device can send out a ***scan response request*** and the peripheral device will respond with the ***scan response data*** in which they can fit additional data that might be interesting. During advertising a peripheral device can send data to many central devices. When a central device connects to a peripheral one the advertising will stop but the two devices can then exchange data in both directions using GATT services and characteristics. ### GATT Generic ATTribute profile is used for sending data back and forth using services and characteristics. A generic protocol called Attribute protocol (ATT) stores Services, characteristics and related data in a lookup table using 16-bit IDs for each entry in the table. A peripheral device can only be connected to one central device. A central device can be connected to multiple peripheral devices. A GATT service is identified by the unique id which can be 16-bit or 128-bit. A service consists of one or more characteristics. For example, the Heart Rate Service 0x180D contains up to 3 characteristics which of the first one is mandatory, **Heart Rate Measurement**, **Body Sensor Location** and **Heart Rate Control Point**. Each characteristic has its own 16-bit or 128-bit UUID. You can either write or read to a characteristic which has a single data point (could also be an array of related values such as x/y/z from an accelerometer). # Installation guide ## Supported platforms - i.MX 8 - CCpilot V1000 - CCpilot V1200 ## Activate Bluetooth Run the following commands in the terminal to enable Bluetooth. ``` sudo systemctl enable bluetooth-attach sudo systemctl enable bluetooth ``` You can check the status of the Bluetooth adapter by running the following command ``` bluetoothctl show ``` ## Setup pulseaudio for Bluetooth If you need to connect Bluetooth speakers/headset to a v1x00 and perhaps have the ability to stream audio to a device you can do the following setup. ### PulseAudio service file Create a file called **pulseaudio.service** in the **/etc/systemd/system/** folder. Use nano or similar and paste in the text below ``` [Unit] Description=Pulseaudio sound server After=avahi-daemon.service network.target [Service] ExecStart=/usr/bin/pulseaudio --system --log-target=syslog ExecReload=/bin/kill -HUP \$MAINPID [Install] WantedBy=multi-user.target ``` After saving it, run the following command to enable the service. ``` sudo systemctl enable pulseaudio ``` ### Setup audio user On v1x00 we need the following setup. Run as root. ``` groupadd pulse-access usermod -G pulse-access -a root usermod -G audio -a root usermod -G pulse-access -a pulse # User of printer group lp has access to bluetooth usermod -G lp -a pulse ``` ### Setup pulseaudio system.pa for Bluetooth The config file /etc/pulse/system.pa need the following two lines. ``` load-module module-bluetooth-policy load-module module-bluetooth-discover ``` Add them last in the **system.pa** config file. Then reboot the device. ### Test audio via bluetoothctl From the command line you will run some commands as the ccs user. If everything above is setup correctly you can start the bluetoothctl by running bluetoothctl Type show to get status of the Bluetooth adapter. If it isn't powered, use power on to power it on. 1. Scan. Use the command scan on to start scanning for new devices 2. Add headphones. Make sure your headphones are in pairing mode. Use the command devices to get a list of found devices. Localize your headphones, I have a pair of HSX headphones and they show up as **Device FC:58:FA:F8:83:20 HSX 3.0** in the list. By using the *mac-address* of your device, pair with the headphones using the command pair FC:58:FA:F8:83:20. After pairing OK it is adviceable to trust the device. Use the command trust FC:58:FA:F8:83:20 to trust the device. Now you should be able to connect to it using the command connect FC:58:FA:F8:83:20. 3. Add streaming device. Repeat the steps taken for the headphones but with your streaming device, usually a phone. Mine is called Device F8:E9:4E:A5:52:A1 Matss iPhone XS. Using that mac-address instead, repeat the above steps with pairing, trusting and connecting. 4. Disable scanning with the command scan off 5. Test it. For example, running spotify you can stream music to the v1x00 which will play it back on the speaker/headphones. #### Trouble shooting If you cannot stream music, first check that both the speaker/headphones and the streaming device are paired, trusted and connected. Still in the bluetoothctl tool. Use the command info FC:58:FA:F8:83:20 but replace the mac-address with your devices mac addresses. For my headphones I get this info. ``` [HSX 3.0]# info FC:58:FA:F8:83:20 Device FC:58:FA:F8:83:20 (public) Name: HSX 3.0 Alias: HSX 3.0 Class: 0x00240418 Icon: audio-card Paired: yes Trusted: yes Blocked: no Connected: yes LegacyPairing: no UUID: Serial Port (00001101-0000-1000-8000-00805f9b34fb) UUID: Headset (00001108-0000-1000-8000-00805f9b34fb) UUID: Audio Sink (0000110b-0000-1000-8000-00805f9b34fb) UUID: A/V Remote Control Target (0000110c-0000-1000-8000-00805f9b34fb) UUID: A/V Remote Control (0000110e-0000-1000-8000-00805f9b34fb) UUID: Handsfree (0000111e-0000-1000-8000-00805f9b34fb) UUID: PnP Information (00001200-0000-1000-8000-00805f9b34fb) Modalias: bluetooth:v000ApFFFFdFFFF ``` Above you also see what profiles the headphones support, stated with different UUID's. If the sound quality is bad make sure you are not in discovery mode. Run scan off # BlueZ BlueZ is the official Bluetooth stack for Linux and we are using version 5.50. The recommended API to communicate with the BlueZ stack from the application is through D-Bus. It provides an API for handling the different profiles. The daemons which handle most of the API is through bluetoothd and obexd. For audio-related profiles you need pulseaudio (which is a sound server system used in modern linux distribution) . You need the *oFono* library for using the hands free profile (HFP) for managing phone calls. For more information on the Bluez API, pulseaudio and oFono see the chapter Further reading. ![Bluez diagram](images/media/image3.png) Above, the bluez protocol stack for Bluetooth classic # D-Bus D-Bus is a message-oriented middleware for inter process communication on the same machine. It is divided into a system bus and a session bus (one for each user session). A message bus daemon will be running which acts like a router. Each application that uses D-Bus has a one to one connection with the message bus daemon. One instance of this message bus daemon runs the system bus, then you will have one instance per user session running the session bus (applications in the user session can communicate with one another). Applications register a unique service name to route messages from one application to another. Example from Qt documentation One app register a service that implements a RPN calculator, also the app provides an object that implements an interface. The client app can then call methods on the object. Looks like this in qt. ```c++ QDBusInterface remoteApp( "com.example.Calculator", "/Calculator Operations","org.mathematics.RPNCalculator" ); remoteApp.call( "PushOperand", 2 ); remoteApp.call( "PushOperand", 2 ); remoteApp.call( "ExecuteOperation", "+" ); QDBusReply reply = remoteApp.call( "PopOperand" ); if ( reply.isValid() ) printf( "%d", reply.value() ); // prints 4 ``` There are four types of messages being sent on the message bus. - Method call message - Method return - Error messages (return an exception caused by invoking a method) - Signal messages (notification that a signal has been emitted) Important elements are - Services (collection of objects providing a specific feature) - Objects (are attached to a service, has a path like /cc/aux/backlight) - Interfaces (implemented by objects, has properties, methods and signals) - Clients (applications that uses a D-Bus service) You usually need the following for invoking a method. - Address, where to find the daemon - Bus name, system bus or session bus - Object path, where to find the object - Interface, the type of the object - Method, the actual action to be performed ![D-Bus interaction diagram](images/media/image4.png) Figure showing how two applications are communicating via D-Bus. ## dbus-monitor For debugging purposes it can be useful to see the very messages being transmitted over D-Bus. To monitor the system bus you can use the command line tool ``` sudo dbus-monitor --system ``` And similar for the session bus ``` dbus-monitor --session ``` # Bluetooth code examples In the following examples we use c++ with Qt and its D-Bus interface classes. You can of course use other programming languages. ## D-Bus and datatypes Sometimes Qt doesn't know how to handle data coming via D-Bus. Using the Q_DECLARE_METATYPE macro with common types you can get around this problem. Below is two commonly recurring datatypes which are good to declare. ```c++ #ifndef TYPEDEFS_H #define TYPEDEFS_H /* define and declare custom types used with bluez D-Bus interfaces */ #include #include typedef QMap InterfacesMap; typedef QMap ObjectsMap; Q_DECLARE_METATYPE(InterfacesMap) Q_DECLARE_METATYPE(ObjectsMap) #endif // TYPEDEFS_H ``` You also need to call the *qDBusRegisterMetaType* function ```c++ /* Register meta types for D-Bus */ qDBusRegisterMetaType(); qDBusRegisterMetaType(); ``` Documentation [The Qt D-Bus Type System](https://doc.qt.io/qt-5/qdbustypesystem.html) ## Important D-Bus services for BlueZ ### org.freedesktop.DBus.ObjectManager This interface defines signals *InterfaceAdded* and *InterfaceRemoved* This signals are emitted when Bluez discovers new devices and when devices are no longer known to bluez. The method GetManagedObjects() is also useful as you can get all of the objects that a D-bus connected process possesses. See example below [Get list of devices](#get-a-list-of-bluetooth-devices) ### org.freedesktop.DBus.Properties This interface is useful for setting and reading properties. It also has a signal called *PropertiesChanged*. Bluez device objects implements this interface so you can easily read and write properties and also react to properties being changed. Look up org.bluez.Device1. See [Latest bluez api](https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/device-api.txt) ## Powering Bluetooth on and off Having some defines variables for commonly used strings can be good practice like below. ```c++ const QString BLUEZ_DBUS_SERVICE = "org.bluez"; const QString BLUEZ_DBUS_PATH = "/org/bluez/hci0"; const QString BLUEZ_DBUS_IF = "org.bluez.Adapter1"; ``` Using the above in code to power the Bluetooth adapter on and off by setting the property *Powered* to true or false. With QDBusInterface you can create a reference to a remote object. In this case it lives on the systemBus. You can check that the reference is valid by calling the `isValid()` function. ```c++ void BluetoothAdapter::powerOff() { QDBusInterface ifc( BLUEZ_DBUS_SERVICE, BLUEZ_DBUS_PATH, BLUEZ_DBUS_IF, QDBusConnection::systemBus()); if (ifc.isValid()) { ifc.setProperty("Powered", false); } } ``` ## Scanning for devices The org.bluez.Adapter1 has methods for starting and stopping discovery (which is essential scanning). Below you have two code examples of starting and stopping discovery. ```c++ void BluetoothAdapter::startDiscovery() { QDBusInterface adapter("org.bluez", "/org/bluez/hci0", "org.bluez.Adapter1", QDBusConnection::systemBus()); QDBusReply reply = adapter.call("StartDiscovery"); if (!reply.isValid()) { qWarning() << Q_FUNC_INFO << "Failed to start discovery: " << reply.error().message(); } } ``` ```c++ void BluetoothAdapter::stopDiscovery() { QDBusInterface adapter("org.bluez", "/org/bluez/hci0", "org.bluez.Adapter1", QDBusConnection::systemBus()); QDBusReply reply = adapter.call("StopDiscovery"); if (!reply.isValid()) { qWarning() << Q_FUNC_INFO << "Failed to stop discovery: " << reply.error().message(); } } ``` ## Pair with device To pair with a specific device you need to know its object path. If you are catching the InterfaceAdded like mentioned earlier you can keep a list of devices to pair with. Your device will likely have another ID than mine **dev_F8_E9_4E_A5_52_A1** It is a good idea to also set *Trusted* to true. ```c++ QDBusInterface *dev = new QDBusInterface("org.bluez", "/org/bluez/hci0/dev_F8_E9_4E_A5_52_A1", "org.bluez.Device1", QDBusConnection::systemBus()); if (dev->property("Paired").toBool()) return; else { dev->asyncCall("Pair"); dev->setProperty("Trusted", true); } } ``` ## How to catch changing properties Below is an example of how to catch properties changing on any Bluetooth device known by bluez. ```c++ m_interface = new QDBusInterface("org.bluez", "/org/bluez/hci0", "org.bluez.Device1", QDBusConnection::systemBus()); if (!m_interface->connection().connect("org.bluez", "", "org.freedesktop.DBus.Properties", "PropertiesChanged", this, SLOT(propertiesChanged(const QString, const QMap, const QStringList)))) { qWarning() << Q_FUNC_INFO << "Failed to connect to D-Bus properties changed signal" << m_interface->lastError().name(); } ``` ## Get a list of Bluetooth devices Below an example of how to get all managed objects from the ObjectManager and pick out the interesting ones for Bluetooth. You would problably want to add devices to some sort of list and for the adapter you might set powerOn. See code below. ```c++ QDBusInterface manager("org.bluez", "/", "org.freedesktop.DBus.ObjectManager", QDBusConnection::systemBus()); QDBusReply>> reply; reply = manager.call("GetManagedObjects"); if (!reply.isValid()) { qWarning() << Q_FUNC_INFO << "Failed to connect to bluez: " << reply.error().message(); return; } auto objects = reply.value(); for (auto i = objects.begin(); i != objects.end(); ++i) { auto ifaces = i.value(); for (auto j = ifaces.begin(); j != ifaces.end(); ++j) { if (j.key() == "org.bluez.Device1") addDevice(i.key()); else if (j.key() == "org.bluez.Adapter1") powerOn(); } } ``` ## Control streaming of music If a device supports the "Audio/Video Remove Control Profile Target" ("0000110c-0000-1000-8000-00805f9b34fb") you can control the playback via the media-api [Bluez media api](https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/media-api.txt) Bluez will add an object like below on D-Bus (here the device has the mac address 48:26:2C:AC:77:B9) **/org/bluez/hci0/dev_48_26_2C_AC_77_B9/player0** Get the D-Bus interface with ```c++ m_objectPath = QDBusObjectPath("/org/bluez/hci0/dev_48_26_2C_AC_77_B9/player0"); p_mediaInterface = new QDBusInterface ("org.bluez", m_objectPath.path(), "org.bluez.MediaPlayer1", QDBusConnection::systemBus()); ``` For keeping a record of what is playing etc. Connect the propertiesChanged signal. ```c++ if(!p_mediaInterface->connection().connect("org.bluez", m_objectPath.path(), "org.freedesktop.DBus.Properties", "PropertiesChanged", this, SLOT(propertiesChanged(const QString, const QMap, const QStringList)))) { qDebug() << "Failed to connect to D-Bus properties changed signal" << p_mediaInterface->lastError().name(); } ``` When the above is setup correctly you can simply control the playback by calling. ```c++ p_mediaInterface->call("Play"); p_mediaInterface->call("Stop"); p_mediaInterface->call("Pause"); p_mediaInterface->call("Next"); p_mediaInterface->call("Previous"); ``` When a property changes you can possible read status and what track is playing etc. ```c++ void BluetoothMediaPlayer::propertiesChanged(const QString path, const QMap changedProperties, const QStringList invalidatedProperties) { Q_UNUSED(invalidatedProperties) Q_UNUSED(path) QMapIterator mapIter(changedProperties); while(mapIter.hasNext()) { mapIter.next(); if(mapIter.key() == "Status") { setStatus(mapIter.value().toString()); } if(mapIter.key() == "Track") { QMap trackData = qdbus_cast >(mapIter.value()); if(!trackData.value("Artist").isNull()) setArtist(trackData.value("Artist").toString()); if(!trackData.value("Title").isNull()) setTitle(trackData.value("Title").toString()); if(!trackData.value("Album").isNull()) setAlbum(trackData.value("Album").toString()); if(!trackData.value("TrackNumber").isNull()) setTrackNumber(trackData.value("TrackNumber").toInt()); if(!trackData.value("NumberOfTracks").isNull()) setNumberOfTracks(trackData.value("NumberOfTracks").toInt()); } } } ``` ## Serial port profile as client Using SPP is like having two devices connected by a serial cable. The QtBluetooth way of finding nearby devices is used here but could be replaced with the D-Bus version. Qt has a subset of Bluetooth-classes. In this example we will use the **QBluetoothDeviceDiscoveryAgent** class to find nearby devices that supports SPP. Below we have created a Discovery class will keep track of nearby devices and has slots for useful signals coming from the QBluetoothDeviceDiscoveryAgent. discovery.h ```c++ #ifndef DISCOVERY_H #define DISCOVERY_H #include #include #include #include #include #include "devicename.h" class Discovery : public QObject { Q_OBJECT Q_PROPERTY(bool scanning READ scanning NOTIFY scanningChanged) Q_PROPERTY(QVariant devices READ devices NOTIFY devicesChanged) Q_PROPERTY(QString info READ info NOTIFY infoChanged) public: explicit Discovery(QObject *parent = nullptr); void setup(); bool scanning() const; QString info() const; QVariant devices(); Q_INVOKABLE void startSearch(); Q_INVOKABLE void sendBytes(long noOfBytes); private slots: void addDevice(const QBluetoothDeviceInfo &device); void scanError(QBluetoothDeviceDiscoveryAgent::Error error); void scanFinished(); void connectedToService(); signals: void scanningChanged(); void devicesChanged(); void infoChanged(); private: void setInfo(QString value); QBluetoothDeviceDiscoveryAgent *m_deviceDiscoveryAgent; QList m_devices; QBluetoothSocket *m_btSocket; QString m_info; bool m_busy; }; #endif // DISCOVERY_H ``` By setting it up like this we can catch discovered devices. When the socket connects our function *connectedToService* will be called. ```c++ void Discovery::setup() { m_deviceDiscoveryAgent = new QBluetoothDeviceDiscoveryAgent(this); connect(m_deviceDiscoveryAgent, &QBluetoothDeviceDiscoveryAgent::deviceDiscovered, this, &Discovery::addDevice); connect(m_deviceDiscoveryAgent, static_cast(&QBluetoothDeviceDiscoveryAgent::error), this, &Discovery::scanError); connect(m_deviceDiscoveryAgent, &QBluetoothDeviceDiscoveryAgent::finished, this, &Discovery::scanFinished); connect(m_deviceDiscoveryAgent, &QBluetoothDeviceDiscoveryAgent::canceled, this, &Discovery::scanFinished); m_btSocket = new QBluetoothSocket(QBluetoothServiceInfo::RfcommProtocol, this); connect(m_btSocket, &QBluetoothSocket::connected, this, &Discovery::connectedToService); } ``` To activate scanning you call ```c++ m_deviceDiscoveryAgent->start(QBluetoothDeviceDiscoveryAgent::DiscoveryMethod::ClassicMethod); ``` The key is to find devices which supports the serial port profile. In the function addDevice we check the UUIDS for matching SerialPortProfile. In the code below we try to connect to the first found Bluetooth device which supports the serial port profile. ```c++ void Discovery::addDevice(const QBluetoothDeviceInfo &device) { m_devices.append(new DeviceName(device)); QList services = device.serviceUuids(); QBluetoothUuid theId; foreach(theId, services) { if (theId == QBluetoothUuid::SerialPort && m_busy == false) { qDebug() << "Found the unit!!! Connecting to " << device.name(); setInfo(tr("Found device support Serial Port %1").arg(device.name())); m_btSocket->connectToService(device.address(), theId); m_busy = true; } } emit devicesChanged(); } ``` When connected via the socket you can start to send and receive bytes. Like writing a message ```c++ char data[] = {"Hello SPP server!"}; m_btSocket->write(data); ``` Read more about QBluetoothSockets at [QBluetoothSocket](https://doc.qt.io/qt-5/qbluetoothsocket.html) ## Serial port profile as server Using Bluez D-Bus API we can use the [profile-api](https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/profile-api.txt) to register that we can handle the SPP as server. Using an adaptor class for the org.bluez.Profile1 interface is the easiest way. Using the tool we can turn an XML description of the interface. Saved as Profile1.xml. ```xml ``` The .h file ```c++ /* * This file was generated by qdbusxml2cpp version 0.8 * Command line was: qdbusxml2cpp Profile1.xml -a bluezprofile1adaptor * * qdbusxml2cpp is Copyright (C) 2020 The Qt Company Ltd. * * This is an auto-generated file. * This file may have been hand-edited. Look for HAND-EDIT comments * before re-generating it. */ #ifndef BLUEZPROFILE1ADAPTOR_H #define BLUEZPROFILE1ADAPTOR_H #include #include #include "typedefs.h" QT_BEGIN_NAMESPACE class QByteArray; template class QList; template class QMap; class QString; class QStringList; class QVariant; QT_END_NAMESPACE /* * Adaptor class for interface org.bluez.Profile1 */ class Profile1Adaptor: public QDBusAbstractAdaptor { Q_OBJECT Q_CLASSINFO("D-Bus Interface", "org.bluez.Profile1") Q_CLASSINFO("D-Bus Introspection", "" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" "") public: Profile1Adaptor(QObject *parent); virtual ~Profile1Adaptor(); public: // PROPERTIES public Q_SLOTS: // METHODS void NewConnection(const QDBusObjectPath &device, const QDBusUnixFileDescriptor &fd, QVariantMap fd_properties); void Release(); void RequestDisconnection(const QDBusObjectPath &device); Q_SIGNALS: // SIGNALS }; #endif ``` The .cpp file ```c++ /* * This file was generated by qdbusxml2cpp version 0.8 * Command line was: qdbusxml2cpp Profile1.xml -a bluezprofile1adaptor * * qdbusxml2cpp is Copyright (C) 2020 The Qt Company Ltd. * * This is an auto-generated file. * Do not edit! All changes made to it will be lost. */ #include "bluezprofile1adaptor.h" #include #include #include #include #include #include #include /* * Implementation of adaptor class Profile1Adaptor */ Profile1Adaptor::Profile1Adaptor(QObject *parent) : QDBusAbstractAdaptor(parent) { // constructor setAutoRelaySignals(true); } Profile1Adaptor::~Profile1Adaptor() { // destructor } void Profile1Adaptor::NewConnection(const QDBusObjectPath &device, const QDBusUnixFileDescriptor &fd, QVariantMap fd_properties) { qDebug() << __PRETTY_FUNCTION__ << device.path(); // handle method call org.bluez.Profile1.NewConnection QMetaObject::invokeMethod(parent(), "NewConnection", Q_ARG(QDBusObjectPath, device), Q_ARG(QDBusUnixFileDescriptor, fd), Q_ARG(QVariantMap, fd_properties)); } void Profile1Adaptor::Release() { // handle method call org.bluez.Profile1.Release QMetaObject::invokeMethod(parent(), "Release"); } void Profile1Adaptor::RequestDisconnection(const QDBusObjectPath &device) { // handle method call org.bluez.Profile1.RequestDisconnection QMetaObject::invokeMethod(parent(), "RequestDisconnection", Q_ARG(QDBusObjectPath, device)); } ``` Using the above adaptor class it is pretty straight forward to implement the functions as in the code below. ```c++ #ifndef SPP_SERVER_H #define SPP_SERVER_H #include #include #include "typedefs.h" #include #include class SPP_server : public QObject { Q_OBJECT Q_PROPERTY(QString message READ message NOTIFY newData) Q_PROPERTY(QString info READ info NOTIFY newInfo) public: explicit SPP_server(QObject *parent = nullptr); const QString message(); const QString info(); Q_INVOKABLE void sendMsg(QString value); public slots: void NewConnection(const QDBusObjectPath &device, const QDBusUnixFileDescriptor &fd, QVariantMap fd_properties); void Release(); void RequestDisconnection(const QDBusObjectPath &device); signals: void newData(); void newInfo(); private: void pollData(); void setMessage(QString value); void setInfo(QString value); int m_fileDescriptor; QString m_message; long m_receivedBytes; QString m_info; }; #endif // SPP_SERVER_H ``` and the .cpp part. From the api-description you can read about the properties being defined in args. ```c++ #include "SPP_Server.h" #include #include #include SPP_server::SPP_server(QObject *parent) : QObject(parent) { m_fileDescriptor = -1; m_receivedBytes = 0; QDBusInterface profileManager("org.bluez", "/org/bluez", "org.bluez.ProfileManager1", QDBusConnection::systemBus()); qDebug() << "SPP_server profilemanager calls"; if (profileManager.isValid()) { QVariant theProfilePath{QVariant::fromValue(QDBusObjectPath("/example/spp"))}; // SerialPortServiceClass QString server_uuid = "00001101-0000-1000-8000-00805f9b34fb"; QMap args; args["AutoConnect"] = true; args["Role"] = "server"; args["Name"] = "Our SPP"; args["Channel"] = QVariant::fromValue((uint16_t)8); args["RequireAuthentication"] = false; args["RequireAutharization"] = false; // Call void RegisterProfile(object profile, string uuid, dict options) QDBusMessage msg{profileManager.call("RegisterProfile", theProfilePath, server_uuid, args)}; if (msg.type() != QDBusMessage::ErrorMessage) { qDebug() << "Registerprofile in SPP_server went well"; } else { qDebug() << msg.errorMessage(); } } else { qDebug() << profileManager.lastError().message(); } QTimer *timer = new QTimer(this); connect(timer, &QTimer::timeout, this, &SPP_server::pollData); timer->start(50); } const QString SPP_server::message() { return m_message; } void SPP_server::sendMsg(QString value) { // Write to client if we have a filedescriptor if (m_fileDescriptor <= 0) return; QByteArray arr = value.toUtf8(); write(m_fileDescriptor, arr.data(), arr.length()); } void SPP_server::setMessage(QString value) { if (value != m_message) { m_message = value; emit newData(); } } const QString SPP_server::info() { return m_info; } void SPP_server::setInfo(QString value) { if (value != m_info) { m_info = value; emit newInfo(); } } void SPP_server::NewConnection(const QDBusObjectPath &device, const QDBusUnixFileDescriptor &fd, QVariantMap fd_properties) { qDebug() << __PRETTY_FUNCTION__ << device.path(); if (fd.isValid()) { // Take it or leave it? if (m_fileDescriptor != -1) close(m_fileDescriptor); m_fileDescriptor = dup(fd.fileDescriptor()); } } void SPP_server::Release() { // Clean up stuff qDebug() << __PRETTY_FUNCTION__; } void SPP_server::RequestDisconnection(const QDBusObjectPath &device) { qDebug() << __PRETTY_FUNCTION__ << device.path(); // Oh.. should disconnect here if (m_fileDescriptor > 0) { close(m_fileDescriptor); m_fileDescriptor = -1; } } void SPP_server::pollData() { // Do we have a connection? if (m_fileDescriptor <= 0) return; // Read data, if small enough, show it UI char buf[65536]; int readCount = read(m_fileDescriptor, buf, sizeof(buf)); if (readCount > 0) { m_receivedBytes += readCount; // Are we receiveing something big? maybe not good to show in ui as text if (readCount < 400) setMessage(QString::fromUtf8(buf, readCount)); // qDebug() << "Total received bytes : " << m_receivedBytes; setInfo(tr("Total received bytes : %1").arg(m_receivedBytes)); } qDebug() << "TimeStamp: " << QDateTime::currentMSecsSinceEpoch() << " Total received bytes : " << m_receivedBytes; } ``` You need to register the spp_server to D-Bus for bluez to route calls to it. in your main.cpp you need something like below. ```c++ SPP_server server; Profile1Adaptor profadaptor(new Profile1Adaptor(&server)); QDBusConnection::systemBus().registerObject("/example/spp", &server); ``` ## Send file to device Files can be sent to a device if it supports the OBEX Object Push Profile. From the UUIDs property of a device you can check if the OPP profile is among them "00001105-0000-1000-8000-00805f9b34fb". To send a file you can use QBluetoothTransferRequest. The receiving device might need some extra attributes about the file being transmitted. Added with the *setAttribute* fuction below. ```c++ void BluetoothTransfer::transferFile(QString address, QString fileName) { qDebug() << __PRETTY_FUNCTION__ << address << fileName; QBluetoothAddress addr(address); QBluetoothTransferRequest request(addr); request.setAttribute(QBluetoothTransferRequest::DescriptionAttribute, QString("A simple txtfile")); request.setAttribute(QBluetoothTransferRequest::NameAttribute, QString("testfile.txt")); request.setAttribute(QBluetoothTransferRequest::TypeAttribute, "text/plain"); QFile *file = new QFile(fileName); request.setAttribute(QBluetoothTransferRequest::LengthAttribute, QVariant(file->size())); request.setAttribute(QBluetoothTransferRequest::TimeAttribute, 1); qDebug() << "Size of file to send: " << QVariant(file->size()); // Ask the transfer manager to send it QBluetoothTransferReply *reply = p_manager->put(request, file); if (reply->error() == QBluetoothTransferReply::NoError) { // Connect to the reply's signals to be informed about the status and do cleanups when done QObject::connect(reply, SIGNAL(finished(QBluetoothTransferReply*)), this, SLOT(transferFinished(QBluetoothTransferReply*))); QObject::connect(reply, SIGNAL(error(QBluetoothTransferReply::TransferError)), this, SLOT(transferError(QBluetoothTransferReply::TransferError))); } else { qWarning() << "Cannot push file" << reply->errorString(); } } ``` By using *QObject::connect(...)* to connect the *finished* signal and *error* signal of **QBluetoothTransferReply** you can keep track of the transfer status. ## Read phonebook from mobile device Similar to sending a file via the Object push profile, quering for phonebook contacts also uses the bluez obex (Object Exchange) under the hood. The api can be viewed here [Bluez Obex-api](https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/obex-api.txt) Using the address of the device you want to read phonebook from (Address is a property of the device-api). We create a new OBEX session where we target pbap (phonebook access). If allowed by the device we will get a session path in return. ```c++ void BluetoothContacts::getContacts(QString deviceAddress) { QDBusInterface client("org.bluez.obex", "/org/bluez/obex", "org.bluez.obex.Client1", QDBusConnection::sessionBus()); QMap args; args["Target"] = "pbap"; QDBusReply reply = client.call("CreateSession", deviceAddress, args); if (!reply.isValid()) { qWarning() << "Failed to create session: " << reply.error(); return; } QString sessionPath = reply.value().path(); qDebug() << "session path:" << sessionPath; listContacts(sessionPath); } ``` In the listContacts function we create a QDBusInterface with the sessionPath as path argument. ```c++ void BluetoothContacts::listContacts(const QString &path) { QDBusInterface client("org.bluez.obex", path, "org.bluez.obex.PhonebookAccess1", QDBusConnection::sessionBus()); QDBusReply reply1 = client.call("Select", "int", "PB"); if (!reply1.isValid()) { qWarning() << "Failed to select internal phonebook: " << reply1.error(); return; } ``` Using the *Select* function (also described in the obex-api) we target the internal phonebook. Below we will then try to pull data from it but only the name and the phone number by providing FN and TEL for the filter. ```c++ QMap filterMap; QStringList filters; QStringList fields; filters << "FN" << "TEL"; fields << "FN" << "TEL"; filterMap["Fields"] = fields; QDBusReply reply = client.call("PullAll", "phonebook.vcf", filterMap); if(!reply.isValid()) { qWarning() << "Failed to pull all contacts: " << reply.error(); return; } m_phonebookTransferPath = reply.value().path(); ``` If the above went well we store the path to the transfer session of the phonebook data. By using a timer we can check the status of the ongoing transfer. ```c++ void BluetoothContacts::checkTransferStatus() { QDBusInterface sessionClient("org.bluez.obex", m_phonebookTransferPath, "org.bluez.obex.Transfer1", QDBusConnection::sessionBus()); QString status = sessionClient.property("Status").toString(); // Eventually the status will be complete or empty "" when transfer is done if(status == QString("complete") || status == "") { qDebug() << "Emitting contactsTransferReady"; emit contactsTransferReady(); } else if(status == QString("error")) { qWarning() << "contacts transfer failed"; } else { QTimer::singleShot(100, this, SLOT(checkTransferStatus())); } } ``` When the transfer is done you can access the phonebook via a QFile object. `QFile file(QDir::homePath() + "/phonebook.vcf");` ## Reading data from a Bluetooth LE sensor Let's assume a real sensor example, such as the BT510 sensor from Laird ![Bt-sensor](images/media/bt510-bluetooth-sensor-award.jpg) [Sentrius BT510 Laird sensor user guide](https://www.lairdconnect.com/documentation/sentrius-bt510-user-guide) This is a beacon like sensor that transmit data to anyone nearby. No connect function call needed. It does transmit data by utilizing the few bytes you can freely define in the 31 byte advertisement package for Bluetooth LE devices. Looking into the guide of this sensor you get a grasp of how to interpret the data. The key for using such a sensor is knowing that the data shows up in **ManufacturerData** property of the device. From the bluez D-Bus [device-api](https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/device-api.txt) **dict ManufacturerData [readonly, optional]** *Manufacturer specific advertisement data. Keys are 16 bits Manufacturer ID followed by its byte array value.* The sensor can send temperature, battery voltage, movement detection and open/close status of a switch. It also have some way of triggering alarms on different temperature levels. This is encoded into the advertisement data. Using D-Bus connect the propertiesChanged signal of the device. Notice that **/org/bluez/hci0/dev_F8_E9_4E_A5_52_A1"** is the path to the device, replace with what you have. ```c++ void BluetoothLE_AdvertSensor::initDBus() { p_SensorDeviceInterface = new QDBusInterface ("org.bluez", "/org/bluez/hci0/dev_F8_E9_4E_A5_52_A1", "org.bluez.Device1", QDBusConnection::systemBus()); if(!p_SensorDeviceInterface->connection().connect("org.bluez", "/org/bluez/hci0/dev_F8_E9_4E_A5_52_A1", "org.freedesktop.DBus.Properties", "PropertiesChanged", this, SLOT(propertiesChanged(const QString, const QMap, const QStringList)))) { qDebug() << "Failed to connect to D-Bus properties changed signal" << p_SensorDeviceInterface->lastError().name(); } } ``` And then look for the ManufacturerData ```c++ void BluetoothLE_AdvertSensor::propertiesChanged(const QString path, const QMap changedProperties, const QStringList invalidatedProperties) { Q_UNUSED(invalidatedProperties) Q_UNUSED(path) QMapIterator mapIter(changedProperties); while(mapIter.hasNext()) { mapIter.next(); // Advertised data is found in the ManufacturerData-property if(mapIter.key() == "ManufacturerData") { QMap theData = qdbus_cast >(mapIter.value()); QMapIterator dataIter(theData); while (dataIter.hasNext()) { dataIter.next(); // For this sensor, the interesting data appear if the key is 0x77 (Laird Company ID 1) if (dataIter.key() == 0x77) { // Check the init-flag if (!initSuccess) setInitSuccess(true); QByteArray arr = dataIter.value().toByteArray(); qDebug() << QDateTime::currentDateTime() << " Length: " << arr.length() << " New values: " << arr; auto data = reinterpret_cast(arr.constData()); // arr[4] and arr[5] == Flags, current state of alarms etc quint16 newState = data[4] + (data[5] << 8); this->setState(newState); // ex New values: "\x01\x00\x00\x00\x07\x81\xD6\xAB<\xB6\x01\xDE\x04\x03\x02v\x02&a\x1F\t\x00\x00\x02" // Data value part of 4 bytes, the LSB is data[19] quint32 theValue = data[19] + (data[20] << 8) + (data[21] << 16) + (data[22] << 24); // arr[12] == Type of data in theValue below switch(data[12]) { case 1: // Temperature qDebug() << "We have a new temperature of " << theValue/100.0f; this->setTemperature(theValue); break; case 2: // Magnet .. has a state qDebug() << "Door is " << (theValue ? "open" : "closed"); break; case 3: // Movement qDebug() << "We have movement!"; break; case 4: // High temp alarm qDebug() << "We have a new high temperature of " << theValue/100.0f; this->setTemperature(theValue); break; case 5: // High temp 2 alarm qDebug() << "We have a new high temperature 2 of " << theValue/100.0f; this->setTemperature(theValue); break; case 6: // Alarm high temp clear qDebug() << "Alarm high temp clear at " << theValue/100.0f; this->setTemperature(theValue); break; case 7: // Alarm low temp 1 qDebug() << "Alarm low temp 1 at " << theValue/100.0f; this->setTemperature(theValue); break; case 8: // Alarm low temp 2 qDebug() << "Alarm low temp 2 at " << theValue/100.0f; this->setTemperature(theValue); break; case 9: // Alarm low temp clear qDebug() << "Alarm low temp clear at " << theValue/100.0f; this->setTemperature(theValue); break; case 10: // Alarm delta temp qDebug() << "Alarm delta temp " << theValue/100.0f; this->setTemperature(theValue); break; case 12: // Battery good qDebug() << "Battery good at " << theValue << "mV"; this->setBattery(theValue); break; case 13: // Advertise on button qDebug() << "Advertise on button at " << theValue << "mV"; this->setBattery(theValue); break; case 16: // Battery bad qDebug() << "Battery bad at " << theValue << "mV"; this->setBattery(theValue); break; } } } } else { qDebug() << "Unknown property changed : " << mapIter.key(); } } } ``` Implementing setters and adding signals for showing data in UI will be the easy part. ## Reading data from heart rate sensor, Bluetooth LE A heart sensor will implement the GATT service 0000180d-0000-1000-8000-00805f9b34fb (HRS - Heart rate service). This is how you usually use Bluetooth LE devices. They expose what services they support via GATT. If you find a device nearby supporting what you are looking for you try to connect to it. You will use the [gatt-api](https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc/gatt-api.txt) to access the service and its characteristics. With the help of GetManagedObjects (See link) you can check that your device has the **org.bluez.GattService1** interface. You can also check that it has the **org.bluez.GattCharacteristic1** interface for reading the different values exposed by the Heart rate service. If you reference a characteristic you can read the UUID of the characteristic like below ```c++ QString HR_MEASUREMENT_CH_ID = "00002a37-0000-1000-8000-00805f9b34fb"; QDBusInterface* characteristic = new QDBusInterface ("org.bluez", aPath, "org.bluez.GattCharacteristic1", QDBusConnection::systemBus()); QString uuid = characteristic->property("UUID").toString(); if (uuid ==this->HR_MEASUREMENT_CH_ID) this->p_hrMeasurement = characteristic; ``` You also need a QDBusInterface to the device like seen before (dev_F8... should be replaced with your device) ```c++ p_heartrateDevice = new QDBusInterface ("org.bluez", "/org/bluez/hci0/dev_F8_E9_4E_A5_52_A1", "org.bluez.Device1", QDBusConnection::systemBus()); ``` You should call Connect on the heartRateDevice and wait for the Connected property to be true. To make the sensor start updating its heart rate value you need to call. ```c++ QDBusReply reply = p_hrMeasurement->call("StartNotify"); ``` ## General attribute variable transfer using Bluetooth LE A Bluetooth LE device can be either peripheral or central. A peripheral device is often some sort of sensor and the central device is a more powerful device, usually a mobile phone or computer. A peripheral device will start off by advertising what kind of services it provides. This communication is unidirectional. To communicate with peripheral BLE device a central device will connect to it. When a connection has been establised only bidirectional communication between the two devices is possible. ### Services and characteristics If we want a custom behaviour, not implementing a known service, we start by defining our own service and characteristics. In the example below we define three variables exposed in our service. ```c++ const QBluetoothUuid ccReadVariableService = QBluetoothUuid(QString("cccccccc-cc16-4841-be7b-cb2d53dee717")); const QBluetoothUuid ccReadVariableCharacteristicGauge1 = QBluetoothUuid(QString("cc02391c-53b5-4ead-b1b9-d3063407e90b")); const QBluetoothUuid ccReadVariableCharacteristicGauge2 = QBluetoothUuid(QString("cc89ba3a-3b03-4e66-9560-14be899d607d")); const QBluetoothUuid ccWriteVariableCharacteristicString = QBluetoothUuid(QString("cc0433cc-b100-45e7-bd91-2c2d3d3f907f")); ``` You can use a online tool for generating random UUID's like we did above. An easy way for transferring data is to have one characteristic per value. A more advanced way would be to implement serial communication via one readable variable and one writeable. ### Server part (the peripheral device) Using some classes from QtBluetooth make this one easy. 1. Setup the device and start advertising our custom service 2. Handle new connection 3. Update the Gauge1 (Readable) value as fast as possible in a loop 4. Update the Gauge2 (Notify) value only when it changes 5. Check if the client writes to the ccWriteVariableCharacteristicString value 6. If the client (central device) disconnects, go back to advertising again. The .h file for the server ```c++ #ifndef SERVER_H #define SERVER_H #include #include class server : public QObject { Q_OBJECT public: explicit server(QObject *parent = nullptr); Q_INVOKABLE void setGauge1Value(qreal value); Q_INVOKABLE void setGauge2Value(quint8 value); Q_PROPERTY(QString info READ info NOTIFY infoChanged) Q_PROPERTY(QString clientMsg READ clientMsg NOTIFY clientMessageChanged) QString info() const; QString clientMsg() const; private: void Setup(); void Value1Limiter(); void WriteValue1ToBT(qreal value); void setInfo(QString info); void setClientMsg(QString value); void characteristicWritten(const QLowEnergyCharacteristic &c, const QByteArray &value); QLowEnergyController* leController; QLowEnergyService* leService; QLowEnergyAdvertisingData* leData; QLowEnergyCharacteristic leCharacteristic1; int m_value1MaxSendPerTimeout = 500; int m_value1Counter = 0; bool m_value1Sleep = false; qreal m_value1LastSet; QString m_info; QString m_msgFromClient; private slots: void ControllerStateChanged(QLowEnergyController::ControllerState state); signals: void infoChanged(); void clientMessageChanged(); }; #endif // SERVER_H ``` Important bits in the cpp file. We setup our service with the three variables we defined UUIDs. ```c++ void server::Setup() { //! [Advertising Data] leData = new QLowEnergyAdvertisingData(); // advertisingData; leData->setDiscoverability(QLowEnergyAdvertisingData::DiscoverabilityGeneral); leData->setIncludePowerLevel(true); leData->setServices(QList() << ccReadVariableService); // Gauge1 QLowEnergyCharacteristicData charData; charData.setUuid(ccReadVariableCharacteristicGauge1); // qreal for first value QByteArray arr1; arr1.setNum(qreal(0)); charData.setValue(arr1); // Read on this one... you can pull a new value about 20 times per second charData.setProperties(QLowEnergyCharacteristic::Read); const QLowEnergyDescriptorData clientConfig(QBluetoothUuid::ClientCharacteristicConfiguration, QByteArray(2, 0)); charData.addDescriptor(clientConfig); // Gauge 2, more of a state value in one single byte, use notify for this one QLowEnergyCharacteristicData charData2; charData2.setUuid(ccReadVariableCharacteristicGauge2); charData2.setValue(QByteArray(1, 1)); charData2.setProperties(QLowEnergyCharacteristic::Notify); const QLowEnergyDescriptorData clientConfig2(QBluetoothUuid::ClientCharacteristicConfiguration, QByteArray(2, 0)); charData2.addDescriptor(clientConfig2); // A short string to be set from client back to server (write) QLowEnergyCharacteristicData charData3; charData3.setUuid(ccWriteVariableCharacteristicString); QByteArray arr3(50,0); charData3.setValue(arr3); charData3.setValueLength(0,50); charData3.setProperties(QLowEnergyCharacteristic::Write); const QLowEnergyDescriptorData clientConfig3(QBluetoothUuid::ClientCharacteristicConfiguration, QByteArray(2, 0)); charData3.addDescriptor(clientConfig3); // Setup the service, we are only using one service with three characteristics QLowEnergyServiceData serviceData; serviceData.setType(QLowEnergyServiceData::ServiceTypePrimary); serviceData.setUuid(ccReadVariableService); serviceData.addCharacteristic(charData); serviceData.addCharacteristic(charData2); serviceData.addCharacteristic(charData3); //! [Start Advertising] leController = QLowEnergyController::createPeripheral(); leService = leController->addService(serviceData); connect(leService, &QLowEnergyService::characteristicChanged, this, &server::characteristicWritten); leController->startAdvertising(QLowEnergyAdvertisingParameters(), *leData, *leData); // Keep track of state changes connect(leController, &QLowEnergyController::stateChanged, this, &server::ControllerStateChanged); connect(leController, static_cast(&QLowEnergyController::error), this, [this](){ qDebug() << "leController got error"; }); // Keep a reference to the first characteristic leCharacteristic1 = leService->characteristic(ccReadVariableCharacteristicGauge1); } ``` To update the qreal-value (our first characteristic) we use this code ```c++ void server::WriteValue1ToBT(qreal value) { QByteArray arr; arr.setNum(value); leService->writeCharacteristic(leCharacteristic1, arr); } ``` Updating our second value, another way of writing without having the reference to the characteristic. This value is also setup as a *Notify* so it behaves like an event to the client side. ```c++ void server::setGauge2Value(quint8 value) { QLowEnergyCharacteristic characteristic = leService->characteristic(ccReadVariableCharacteristicGauge2); QByteArray arr(1,value); leService->writeCharacteristic(characteristic, arr); } ``` Our third variable is a string that is writeable from the client (so we don't have any write-code on the server part) When the central device writes we can catch the value in: ```c++ void server::characteristicWritten(const QLowEnergyCharacteristic &c, const QByteArray &value) { // Something was written from client side if (c.uuid() == ccWriteVariableCharacteristicString) { // Update msg from client qDebug() << "Client message " << QString::fromUtf8(value); setClientMsg(QString::fromUtf8(value)); } } ``` Also in the setup we catch when the state of the low energy adaptor changes. ```c++ void server::ControllerStateChanged(QLowEnergyController::ControllerState state) { // State of leController changes qDebug() << "ControllerStateChanged to state " << state; // Restart advertising after a disconnect if (state != QLowEnergyController::AdvertisingState && state != QLowEnergyController::ConnectedState) { // Start advertisting again leController->startAdvertising(QLowEnergyAdvertisingParameters(), *leData, *leData); } } ``` ### Client part (the central device) As in other examples we can use the QBluetoothDeviceDiscoveryAgent class and call our function addDevice when devices are discovered. ```c++ m_deviceDiscoveryAgent = new QBluetoothDeviceDiscoveryAgent(this); m_deviceDiscoveryAgent->setLowEnergyDiscoveryTimeout(5000); connect(m_deviceDiscoveryAgent, &QBluetoothDeviceDiscoveryAgent::deviceDiscovered, this, &Discovery::addDevice); ``` When starting to scan we supply the LowEnergyMethod to the agent. ```c++ void Discovery::startSearch() { // Find some Bluetooth LE devices m_deviceDiscoveryAgent->start(QBluetoothDeviceDiscoveryAgent::DiscoveryMethod::LowEnergyMethod); emit scanningChanged(); } ``` We can filter out Bluetooth LE devices and check if the service uuid matches our custom *ccReadVariableService*. If we have a match we call StartConnect on our Client class described after this code block. ```c++ void Discovery::addDevice(const QBluetoothDeviceInfo &device) { // If device is LowEnergy-device, add it to the list if (device.coreConfigurations() & QBluetoothDeviceInfo::LowEnergyCoreConfiguration) { m_devices.append(new DeviceName(device)); QList services = device.serviceUuids(); QBluetoothUuid theId; foreach(theId, services) { if (theId == ccReadVariableService) { m_client->setInfo("Found device " + device.name() ); m_client->StartConnect(device); } } } } ``` A simple client class is created. The .h file ```c++ #ifndef CLIENT_H #define CLIENT_H #include #include #include #include #include "typedefs.h" // Read data from a server using Bluetooth LE class Client : public QObject { Q_OBJECT Q_PROPERTY(qreal gauge1 READ gauge1 NOTIFY gauge1Changed) Q_PROPERTY(quint8 gauge2 READ gauge2 NOTIFY gauge2Changed) Q_PROPERTY(QString info READ info NOTIFY infoChanged) Q_PROPERTY(bool connected READ connected NOTIFY connectedChanged) public: explicit Client(QObject *parent = nullptr); Q_INVOKABLE void boostConnection(); Q_INVOKABLE void sayHiToServer(); bool connected() const; qreal gauge1() const; quint8 gauge2() const; QString info() const; void StartConnect(const QBluetoothDeviceInfo &device); void setInfo(QString info); signals: void gauge1Changed(); // The QML part will be interested void gauge2Changed(); // The QML part will be interested void infoChanged(); void connectedChanged(); private: void setConnected(bool value); void setGauge1(qreal value); void setGauge2(quint8 value); void serviceDiscovered(const QBluetoothUuid &); void servicescanDone(); void gotError(QLowEnergyController::Error err); void gotServiceError(QLowEnergyService::ServiceError err); void serviceStateChanged(QLowEnergyService::ServiceState s); void characteristicRead(const QLowEnergyCharacteristic &c, const QByteArray &value); void updateDataValues(const QLowEnergyCharacteristic &c, const QByteArray &value); void countTraffic(); void pollData(); qreal m_gauge1; quint8 m_gauge2; QString m_info; bool m_connected; bool m_pendingRead; QBluetoothLocalDevice m_localDevice; QLowEnergyController *m_control = nullptr; QLowEnergyService *m_service = nullptr; QLowEnergyDescriptor m_notificationDesc; int m_recieveCounter=0; int m_beginRecieveCounter=0; bool m_serviceDiscovered = false; int m_debugCounter = 0; int m_sayHiCounter = 0; int m_readTimeoutCounter = 0; }; #endif // CLIENT_H ``` Important parts of cpp file ```c++ #include "client.h" #include #include #include #include #include #include Client::Client(QObject *parent) : QObject(parent) { m_control = nullptr; m_gauge1 = 0; m_gauge2 = 0; m_info = "Client information"; m_connected = false; m_pendingRead = false; m_localDevice.powerOn(); m_localDevice.setHostMode(QBluetoothLocalDevice::HostDiscoverable); QTimer *timer = new QTimer(this); connect(timer, &QTimer::timeout, this, &Client::pollData); timer->start(10); QTimer *timer2 = new QTimer(this); connect(timer2, &QTimer::timeout, this, &Client::countTraffic); timer2->start(1000); } void Client::sayHiToServer() { if (m_connected) { QStringList lst = {"Hi", "Hello","Godday","Morning","Whats up"}; int choose = QRandomGenerator::global()->bounded(0, lst.length()); QString str = QVariant(m_sayHiCounter++).toString() + " " + lst.at(choose); qDebug() << "Say hi to server: " << str; // Write string to server writeVariable QLowEnergyCharacteristic characteristic = m_service->characteristic(ccWriteVariableCharacteristicString); QByteArray arr = str.toUtf8(); m_service->writeCharacteristic(characteristic, arr, QLowEnergyService::WriteWithoutResponse); } } qreal Client::gauge1() const { return m_gauge1; } quint8 Client::gauge2() const { return m_gauge2; } QString Client::info() const { return m_info; } bool Client::connected() const { return m_connected; } void Client::StartConnect(const QBluetoothDeviceInfo &device) { // Init m_control = QLowEnergyController::createCentral(device); // Connect some interesting parts connect(m_control, &QLowEnergyController::discoveryFinished, this, &Client::servicescanDone); connect(m_control, &QLowEnergyController::serviceDiscovered, this, &Client::serviceDiscovered); connect(m_control, static_cast(&QLowEnergyController::error), this, &Client::gotError); connect(m_control, &QLowEnergyController::connected, this, [this]() { setInfo("Controller connected. Search services..."); m_control->discoverServices(); }); connect(m_control, &QLowEnergyController::disconnected, this, [this]() { m_serviceDiscovered = false; setInfo("LowEnergy controller disconnected"); }); connect(m_control, &QLowEnergyController::connectionUpdated, this, [this](const QLowEnergyConnectionParameters para) { // Never gets called? qDebug() << "connectionUpdated: " << para.minimumInterval(); setInfo("connectionUpdated"); }); // Try to connect qDebug() << "Connecting to device!!"; m_control->connectToDevice(); } void Client::serviceDiscovered(const QBluetoothUuid &uuid) { // Check order qDebug() << __PRETTY_FUNCTION__; qDebug() << uuid; } void Client::servicescanDone() { qDebug() << __PRETTY_FUNCTION__; // Delete old service if available if (m_service) { delete m_service; m_service = nullptr; } m_service = m_control->createServiceObject(ccReadVariableService, this); if (m_service) { connect(m_service, &QLowEnergyService::characteristicChanged, this, &Client::updateDataValues); connect(m_service, &QLowEnergyService::stateChanged, this, &Client::serviceStateChanged); connect(m_service, &QLowEnergyService::characteristicRead, this, &Client::characteristicRead); connect(m_service, static_cast(&QLowEnergyService::error), this, &Client::gotServiceError); m_service->discoverDetails(); } else { setInfo("m_service null"); } } void Client::gotError(QLowEnergyController::Error err) { qDebug() << err; setInfo("Failed to connect"); } void Client::gotServiceError(QLowEnergyService::ServiceError err) { qDebug() << "Service error: " << err; } void Client::serviceStateChanged(QLowEnergyService::ServiceState s) { if (s == QLowEnergyService::ServiceDiscovered) { m_serviceDiscovered = true; setConnected(true); // Send notification start, needed for a characteristic with Notify const QLowEnergyCharacteristic hrChar = m_service->characteristic(ccReadVariableCharacteristicGauge2); m_notificationDesc = hrChar.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration); if (m_notificationDesc.isValid()) m_service->writeDescriptor(m_notificationDesc, QByteArray::fromHex("0100")); else setInfo("m_notificationDesc not valid"); } else { setConnected(false); } } void Client::setGauge1(qreal value) { if (value != m_gauge1) { m_gauge1 = value; emit gauge1Changed(); } } void Client::setGauge2(quint8 value) { if (value != m_gauge2) { m_gauge2 = value; } } void Client::characteristicRead(const QLowEnergyCharacteristic &c, const QByteArray &value) { // I guess this is the answer from our polling of gauge1 if (c.uuid() == ccReadVariableCharacteristicGauge1) { // Only gets called once when connected (a read variable), but also if request read setGauge1(value.toDouble()); m_recieveCounter++; m_pendingRead = false; } } void Client::updateDataValues(const QLowEnergyCharacteristic &c, const QByteArray &value) { // Depending on the type of c... update correct value if (c.uuid() == ccReadVariableCharacteristicGauge2) { // Gets called everytime the value changes via notify, but not when first connected setGauge2((quint8)value[0]); } } void Client::countTraffic() { // Timer calls this once a second if (m_recieveCounter > 0) qDebug() << "Recieve count " << m_recieveCounter << " begin count " << m_beginRecieveCounter; m_recieveCounter = 0; m_beginRecieveCounter = 0; } void Client::pollData() { // if connected.. read data here if (m_serviceDiscovered) { if (m_pendingRead == false || m_readTimeoutCounter > 100) { const QLowEnergyCharacteristic char1 = m_service->characteristic(ccReadVariableCharacteristicGauge1); m_service->readCharacteristic(char1); m_pendingRead = true; m_beginRecieveCounter++; m_readTimeoutCounter = 0; } else m_readTimeoutCounter++; } } void Client::setInfo(QString info) { if (info != m_info) { m_info = info; emit infoChanged(); } } void Client::setConnected(bool value) { if (value != m_connected) { m_connected = value; emit connectedChanged(); } } ``` ## D-Bus abstraction with Qt If you have an XML-file describing a D-Bus interface you can use a special tool called qdbusxml2cpp for generating a cpp class which contains Qt signal and slots for the interface. Generating the XML can be done via the dbus-send tool using this syntax. ``` dbus-send --session --print-reply --dest=service.name /obj/path org.freedesktop.DBus.Introspectable.Introspect > name.xml ``` # Bluetoothctl Bluetoothctl is a command line tool to interact with the bluetoothd daemon and is very useful for debugging and testing different scenarios. You can access special menus for *gatt*, *scan* and *advertise,* which are used for Bluetooth LE. ![Bluetoothctl](images/media/image5.png) ## Advertise menu ![Advertise menu](./images/media/image6.png) ## Scan menu ![Scan menu](images/media/image7.png) ## Gatt menu ![Gatt menu](./images/media/image8.png) # Obexctl Obexctl is a command line tool for interacting with the obexd daemon which is used for sending/receiving files via Bluetooth. ![Obexctl menu](images/media/image9.png) Note: Obex is not supported on Apple products, at least not iPhone. # Further reading 1. [Introduction to Bluetooth Classic](https://www.argenox.com/library/bluetooth-classic/introduction-to-bluetooth-classic/) 2. [Introduction to Bluetooth LE](https://www.argenox.com/library/bluetooth-low-energy/introduction-to-bluetooth-low-energy-v4-0/) 3. [Introduction to Bluetooth LE - Developer Help](https://microchipdeveloper.com/wireless:ble-introduction) 4. [Qt D-Bus](https://doc.qt.io/qt-5/qtdbus-index.html) 5. [Performance Evaluation of Bluetooth LE: A Systematic Review](https://www.ncbi.nlm.nih.gov/pmc/articles/PMC5751532/) 6. [Pulseaudio and oFono](https://01.org/ofono) 7. [PulseAudio under the hood](https://gavv.github.io/articles/pulseaudio-under-the-hood/) 8. [Latest Bluez D-BUS API](https://git.kernel.org/pub/scm/bluetooth/bluez.git/tree/doc) 9. [Qt D-Bus chat example](https://doc.qt.io/qt-5/qtdbus-chat-example.html)