Bluetooth Low Energy Heart Rate Server
An example demonstrating how to set up and advertise a GATT service. The example demonstrates the use of the Qt Bluetooth Low Energy classes related to peripheral (slave) functionality.
The Bluetooth Low Energy Heart Rate Server is a command-line application that shows how to develop a Bluetooth GATT server using the Qt Bluetooth API. The application covers setting up a service, advertising it and notifying clients about changes to characteristic values.
The example makes use of the following Qt classes:
- QLowEnergyAdvertisingData
- QLowEnergyAdvertisingParameters
- QLowEnergyServiceData
- QLowEnergyCharacteristicData
- QLowEnergyDescriptorData
- QLowEnergyController
- QLowEnergyService
The example implements a server application, which means it has no graphical user interface. To visualize what it is doing, you can use the Heart Rate Game example, which is basically the client-side counterpart to this application.
Checking Bluetooth Permission
Before the application can create a service and start advertising, we have to check if the application has a permission to use Bluetooth.
auto permissionStatus = app.checkPermission(QBluetoothPermission{});
Request Bluetooth Permission
If the Bluetooth authorization status is undetermined, we have to request a permission to use Bluetooth.
if (permissionStatus == Qt::PermissionStatus::Undetermined) { qInfo("Requesting Bluetooth permission ..."); app.requestPermission(QBluetoothPermission{}, [&permissionStatus](const QPermission &permission){ qApp->exit(); permissionStatus = permission.status(); }); // Now, wait for permission request to resolve. app.exec(); }
Setting up Advertising Data and Parameters
Two classes are used to configure the advertising process:
- QLowEnergyAdvertisingData specifies which information is to be broadcasted
- QLowEnergyAdvertisingParameters is used for specific aspects such as setting the advertising interval or controlling which devices are allowed to connect.
In our example, we simply use the default parameters.
The information contained in the QLowEnergyAdvertisingData will be visible to other devices that are currently scanning. They can use it to decide whether they want to establish a connection or not. In our example, we include the type of service we offer, a name that adequately describes our device to humans, and the transmit power level of the device. The latter is often useful to potential clients, because they can tell how far away our device is by comparing the received signal strength to the advertised one.
Note: Space for the advertising data is very limited (only 31 bytes in total), so variable-length data such as the device name should be kept as short as possible.
QLowEnergyAdvertisingData advertisingData; advertisingData.setDiscoverability(QLowEnergyAdvertisingData::DiscoverabilityGeneral); advertisingData.setIncludePowerLevel(true); advertisingData.setLocalName("HeartRateServer"); advertisingData.setServices(QList<QBluetoothUuid>() << QBluetoothUuid::ServiceClassUuid::HeartRate);
Setting up Service Data
Next we configure the kind of service we want to offer. We use the Heart Rate service as defined in the Bluetooth specification in its minimal form, that is, consisting only of the Heart Rate Measurement characteristic. This characteristic must support the Notify property (and no others), and it needs to have a Client Characteristic Configuration descriptor, which enables clients to register to get notified about changes to characteristic values. We set the initial heart rate value to zero, as it cannot be read anyway (the only way the client can get the value is via notifications).
QLowEnergyCharacteristicData charData; charData.setUuid(QBluetoothUuid::CharacteristicType::HeartRateMeasurement); charData.setValue(QByteArray(2, 0)); charData.setProperties(QLowEnergyCharacteristic::Notify); const QLowEnergyDescriptorData clientConfig(QBluetoothUuid::DescriptorType::ClientCharacteristicConfiguration, QByteArray(2, 0)); charData.addDescriptor(clientConfig); QLowEnergyServiceData serviceData; serviceData.setType(QLowEnergyServiceData::ServiceTypePrimary); serviceData.setUuid(QBluetoothUuid::ServiceClassUuid::HeartRate); serviceData.addCharacteristic(charData);
Advertising and Listening for Incoming Connections
Now that all the data has been set up, we can start advertising. First we create a QLowEnergyController object in the peripheral role and use it to create a (dynamic) QLowEnergyService object from our (static) QLowEnergyServiceData. Then we call QLowEnergyController::startAdvertising(). Note that we hand in our QLowEnergyAdvertisingData twice: The first argument acts as the actual advertising data, the second one as the scan response data. They could transport different information, but here we don't have a need for that. We also pass a default-constructed instance of QLowEnergyAdvertisingParameters, because the default advertising parameters are fine for us. If a client is interested in the advertised service, it can now establish a connection to our device. When that happens, the device stops advertising and the QLowEnergyController::connected() signal is emitted.
Note: When a client disconnects, advertising does not resume automatically. If you want that to happen, you need to connect to the QLowEnergyController::disconnected() signal and call QLowEnergyController::startAdvertising() in the respective slot.
bool errorOccurred = false; const std::unique_ptr<QLowEnergyController> leController(QLowEnergyController::createPeripheral()); auto errorHandler = [&leController, &errorOccurred](QLowEnergyController::Error errorCode) { qWarning().noquote().nospace() << errorCode << " occurred: " << leController->errorString(); if (errorCode != QLowEnergyController::RemoteHostClosedError) { qWarning("Heartrate-server quitting due to the error."); errorOccurred = true; QCoreApplication::quit(); } }; QObject::connect(leController.get(), &QLowEnergyController::errorOccurred, errorHandler); std::unique_ptr<QLowEnergyService> service(leController->addService(serviceData)); leController->startAdvertising(QLowEnergyAdvertisingParameters(), advertisingData, advertisingData); if (errorOccurred) return -1;
Providing the Heartrate
So far, so good. But how does a client actually get at the heart rate? This happens by regularly updating the value of the respective characteristic in the QLowEnergyService object that we received from the QLowEnergyController in the code snippet above. The source of the heart rate would normally be some kind of sensor, but in our example, we just make up values that we let oscillate between 60 and 100. The most important part in the following code snippet is the call to QLowEnergyService::writeCharacteristic. If a client is currently connected and has enabled notifications by writing to the aforementioned Client Characteristic Configuration
, it will get notified about the new value.
QTimer heartbeatTimer; quint8 currentHeartRate = 60; enum ValueChange { ValueUp, ValueDown } valueChange = ValueUp; const auto heartbeatProvider = [&service, ¤tHeartRate, &valueChange]() { QByteArray value; value.append(char(0)); // Flags that specify the format of the value. value.append(char(currentHeartRate)); // Actual value. QLowEnergyCharacteristic characteristic = service->characteristic(QBluetoothUuid::CharacteristicType::HeartRateMeasurement); Q_ASSERT(characteristic.isValid()); service->writeCharacteristic(characteristic, value); // Potentially causes notification. if (currentHeartRate == 60) valueChange = ValueUp; else if (currentHeartRate == 100) valueChange = ValueDown; if (valueChange == ValueUp) ++currentHeartRate; else --currentHeartRate; }; QObject::connect(&heartbeatTimer, &QTimer::timeout, heartbeatProvider); heartbeatTimer.start(1000);