Warning
This section contains snippets that were automatically translated from C++ to Python and may contain errors.
Bluetooth Low Energy Overview#
The Qt Bluetooth Low Energy API enables communication between Bluetooth Low Energy devices.
The Qt Bluetooth Low Energy API supports the peripheral/server and central/client roles. It is supported on all major Qt platforms. The only exception is the missing peripheral role support on Windows.
What Is Bluetooth Low Energy#
Bluetooth Low Energy, also known as Bluetooth Smart, is a wireless computer network technology, which was officially introduced in 2011. It works on the same 2.4 GHz frequency as ”classic” Bluetooth. The main difference is, as stated by its technology name, low energy consumption. It provides an opportunity for Bluetooth Low Energy devices to operate for months, even years, on coin-cell batteries. The technology was introduced by Bluetooth v4.0 . Devices which support this technology are called Bluetooth Smart Ready Devices. The key features of the technology are:
Ultra-low peak, average and idle mode power consumption
Ability to run for years on standard, coin-cell batteries
Low cost
Multi-vendor interoperability
Enhanced range
Bluetooth Low Energy uses a client-server architecture. The server (also known as peripheral) offers services such as temperature or heart rate and advertises them. The client (known as central device) connects to the server and reads the values advertised by the server. An example might be an apartment with Bluetooth Smart Ready sensors such as a thermostat, humidity or pressure sensor. Those sensors are peripheral devices advertising the environment values of the apartment. At the same time a mobile phone or computer might connect to those sensors, retrieve their values and present them as part of a larger environment control application to the user.
Basic Service Structure#
Bluetooth Low Energy is based on two protocols: ATT (Attribute Protocol) and GATT (Generic Attribute Profile). They specify the communication layers used by every Bluetooth Smart Ready device.
ATT Protocol#
The basic building block of ATT is an attribute. Each attribute consists of three elements:
a value - the payload or desirable piece of information
a UUID - the type of attribute (used by GATT)
a 16-bit handle - a unique identifier for the attribute
The server stores the attributes and the client uses the ATT protocol to read and write values on the server.
GATT Profile#
GATT defines grouping for a set of attributes by applying a meaning to predefined UUIDs. The table below shows an example service exposing a heart rate on a particular day. The actual values are stored inside the two characteristics:
Handle
UUID
Value
Description
0x0001
0x2800
UUID 0x180D
Begin Heart Rate service
0x0002
0x2803
UUID 0x2A37, Value handle: 0x0003
Characteristic of type Heart Rate Measurement (HRM)
0x0003
0x2A37
65 bpm
Heart rate value
0x0004
0x2803
UUID 0x2A08, Value handle: 0x0005
Characteristic of type Date Time
0x0005
0x2A08
18/08/2014 11:00
Date and Time of the measurement
0x0006
0x2800
UUID xxxxxx
Begin next service
…
…
…
…
GATT specifies that the above used UUID 0x2800
marks the begin of a service definition. Every attribute following 0x2800
is part of the service until the next 0x2800
or the end is encountered. In similar ways the well known UUID 0x2803
states that a characteristic is to be found and each of the characteristics has a type defining the nature of the value. The example above uses the UUIDs 0x2A08
(Date Time) and 0x2A37
(Heart Rate Measurement). Each of the above UUIDs is defined by the Bluetooth Special Interest Group . and can be found in the GATT specifications . While it is advisable to use pre-defined UUIDs where available it is entirely possible to use new and not yet used UUIDs for characteristic and service types.
In general, each service may consist of one or more characteristics. A characteristic contains data and can be further described by descriptors, which provide additional information or means of manipulating the characteristic. All services, characteristics and descriptors are recognized by their 128-bit UUID. Finally, it is possible to include services inside of services (see picture below).
Using Qt Bluetooth Low Energy API#
This section describes how to use the Bluetooth Low Energy API provided by Qt. On the client side, the API permits creating connections to peripheral devices, discovering their services, as well as reading and writing data stored on the device. On the server side, it allows to set up services, advertise them, and get notified when the client writes characteristics. The example code below is taken from the Heart Rate Game and Heart Rate Server examples.
Establishing a Connection#
To be able to read and write the characteristics of the Bluetooth Low Energy peripheral device, it is necessary to find and connect the device. This requires the peripheral device to advertise its presence and services. We start the device discovery with the help of the QBluetoothDeviceDiscoveryAgent
class. We connect to its deviceDiscovered()
signal and start the search with start()
:
m_deviceDiscoveryAgent = QBluetoothDeviceDiscoveryAgent(self) m_deviceDiscoveryAgent.setLowEnergyDiscoveryTimeout(15000) m_deviceDiscoveryAgent.deviceDiscovered.connect( self.addDevice) m_deviceDiscoveryAgent.errorOccurred.connect( self.scanError) m_deviceDiscoveryAgent.finished.connect( self.scanFinished) m_deviceDiscoveryAgent.canceled.connect( self.scanFinished) m_deviceDiscoveryAgent.start(QBluetoothDeviceDiscoveryAgent.LowEnergyMethod)
Since we are only interested in Low Energy devices we filter the device type within the receiving slot. The device type can be ascertained using the coreConfigurations()
flag. The deviceDiscovered()
signal may be emitted multiple times for the same device as more details are discovered. Here we match these device discoveries so that the user only sees the individual devices:
def addDevice(self, device): # If device is LowEnergy-device, add it to the list if device.coreConfigurations() QBluetoothDeviceInfo.LowEnergyCoreConfiguration: devInfo = DeviceInfo(device) it = std::find_if(m_devices.begin(), m_devices.end(), [devInfo](DeviceInfo dev) { return devInfo.getAddress() == dev.getAddress() }) if it == m_devices.end(): m_devices.append(devInfo) else: oldDev = it it = devInfo del oldDev setInfo(tr("Low Energy device found. Scanning more...")) setIcon(IconProgress) #...
Once the address of the peripheral device is known we use the QLowEnergyController
class. This class is the entry point for all Bluetooth Low Energy development. The constructor of the class accepts the remote device’s QBluetoothAddress
. Finally we set up the customary slots and directly connect to the device using connectToDevice()
:
m_control = QLowEnergyController.createCentral(m_currentDevice.getDevice(), self) m_control.serviceDiscovered.connect( self.serviceDiscovered) m_control.discoveryFinished.connect( self.serviceScanDone) m_control.errorOccurred.connect(this, [self](QLowEnergyController.Error error) { Q_UNUSED(error) setError("Cannot connect to remote device.") setIcon(IconError) }) m_control.connected.connect(this, [this]() { setInfo("Controller connected. Search services...") setIcon(IconProgress) m_control.discoverServices() }) m_control.disconnected.connect(this, [this]() { setError("LowEnergy controller disconnected") setIcon(IconError) }) # Connect m_control.connectToDevice()
Service Search#
The above code snippet shows how the application initiates the service discovery once the connection has been established.
The serviceDiscovered()
slot below is triggered as a result of the serviceDiscovered()
signal and provides an intermittent progress report. Since we are talking about the heart listener app which monitors HeartRate devices in the vicinity we ignore any service that is not of type HeartRate
.
def serviceDiscovered(self, gatt): if gatt == QBluetoothUuid(QBluetoothUuid.ServiceClassUuid.HeartRate): setInfo("Heart Rate service discovered. Waiting for service scan to be done...") setIcon(IconProgress) m_foundHeartRateService = True
Eventually the discoveryFinished()
signal is emitted to indicate the successful completion of the service discovery. Provided a HeartRate service was found, a QLowEnergyService
instance is created to represent the service. The returned service object provides the required signals for update notifications and the discovery of service details is triggered using discoverDetails()
:
# If heartRateService found, create new service if m_foundHeartRateService: m_service = m_control.createServiceObject(QBluetoothUuid(QBluetoothUuid.ServiceClassUuid.HeartRate), self) if m_service: m_service.stateChanged.connect(self.serviceStateChanged) m_service.characteristicChanged.connect(self.updateHeartRateValue) m_service.descriptorWritten.connect(self.confirmedDescriptorWrite) m_service.discoverDetails() else: setError("Heart Rate Service not found.") setIcon(IconError)
During the detail search the service’s state()
transitions from RemoteService
to RemoteServiceDiscovering
and eventually ends with RemoteServiceDiscovered
:
def serviceStateChanged(self, s): if s == QLowEnergyService.RemoteServiceDiscovering: setInfo(tr("Discovering services...")) setIcon(IconProgress) break elif s == QLowEnergyService.RemoteServiceDiscovered: setInfo(tr("Service discovered.")) setIcon(IconBluetooth) hrChar = m_service.characteristic(QBluetoothUuid(QBluetoothUuid.CharacteristicType.HeartRateMeasurement)) if not hrChar.isValid(): setError("HR Data not found.") setIcon(IconError) break m_notificationDesc = hrChar.descriptor(QBluetoothUuid.DescriptorType.ClientCharacteristicConfiguration) if m_notificationDesc.isValid(): m_service.writeDescriptor(m_notificationDesc, QByteArray.fromHex("0100")) break else: #nothing for now break aliveChanged.emit()
Interaction with the Peripheral Device#
In the code example above, the desired characteristic is of type HeartRateMeasurement
. Since the application measures the heart rate changes, it must enable change notifications for the characteristic. Note that not all characteristics provide change notifications. Since the HeartRate characteristic has been standardized it is possible to assume that notifications can be received. Ultimately properties()
must have the Notify
flag set and a descriptor of type ClientCharacteristicConfiguration
must exist to confirm the availability of an appropriate notification.
Finally, we process the value of the HeartRate characteristic, as per Bluetooth Low Energy standard:
def updateHeartRateValue(self, c, value): # ignore any other characteristic change -> shouldn't really happen though if c.uuid() != QBluetoothUuid(QBluetoothUuid.CharacteristicType.HeartRateMeasurement): return data = quint8(value.constData()) flags = data #Heart Rate hrvalue = 0 if flags 0x1: # HR 16 bit? otherwise 8 bit hrvalue = int(qFromLittleEndian<quint16>(data[1])) else: hrvalue = int(data[1]) addMeasurement(hrvalue)
In general a characteristic value is a series of bytes. The precise interpretation of those bytes depends on the characteristic type and value structure. A significant number has been standardized by the Bluetooth SIG whereas others may follow a custom protocol. The above code snippet demonstrates how to the read the standardized HeartRate value.
Advertising Services#
If we are implementing a GATT server application on a peripheral device, we need to define the services we want to offer to central devices and advertise them:
advertisingData = QLowEnergyAdvertisingData() advertisingData.setDiscoverability(QLowEnergyAdvertisingData.DiscoverabilityGeneral) advertisingData.setIncludePowerLevel(True) advertisingData.setLocalName("HeartRateServer") advertisingData.setServices(QList<QBluetoothUuid>() << QBluetoothUuid.ServiceClassUuid.HeartRate) errorOccurred = False 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
Now potential clients can connect to our device, discover the provided service and register themselves to get notified of changes to the characteristic value. This part of the API was already covered by the above sections.
Implementing a Service on the Peripheral Device#
The first step is to define the service, its characteristics and descriptors. This is achieved using the QLowEnergyServiceData
, QLowEnergyCharacteristicData
and QLowEnergyDescriptorData
classes. These classes act as containers or building blocks for the essential information that comprises the to-be-defined Bluetooth Low Energy service. The code snippet below defines a simple HeartRate service which publishes the measured beats per minute. An example where such a service could be used is a wrist watch.
charData = QLowEnergyCharacteristicData() charData.setUuid(QBluetoothUuid.CharacteristicType.HeartRateMeasurement) charData.setValue(QByteArray(2, 0)) charData.setProperties(QLowEnergyCharacteristic.Notify) QLowEnergyDescriptorData clientConfig(QBluetoothUuid.DescriptorType.ClientCharacteristicConfiguration, QByteArray(2, 0)) charData.addDescriptor(clientConfig) serviceData = QLowEnergyServiceData() serviceData.setType(QLowEnergyServiceData.ServiceTypePrimary) serviceData.setUuid(QBluetoothUuid.ServiceClassUuid.HeartRate) serviceData.addCharacteristic(charData)
The resulting serviceData
object can be published as described in the Advertising Services section above. Despite the partial information overlap between the information wrapped by QLowEnergyServiceData
and QLowEnergyAdvertisingData
the two classes serve two very different tasks. The advertising data is published to nearby devices and often limited in scope due to its size restriction of 29 bytes. Therefore they are not always 100% complete. By comparison the service data contained inside of QLowEnergyServiceData
provides the complete set of service data and only becomes visible to the connecting client when a connection with an active service discovery has been performed.
The next section demonstrates how the service can update the heart rate value. Depending on the nature of the service it may have to comply with the official service definition as defined on https://www.bluetooth.org . Other services may be completely custom. The heart rate service was adopted and its specification can be found under https://www.bluetooth.com/specifications/adopted-specifications .
heartbeatTimer = QTimer() currentHeartRate = 60 ValueChange = { ValueUp, ValueDown } valueChange = ValueUp auto heartbeatProvider = [service, currentHeartRate, valueChange]() { value = QByteArray() 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 elif currentHeartRate == 100: valueChange = ValueDown if valueChange == ValueUp: currentHeartRate = currentHeartRate + 1 else: currentHeartRate = currentHeartRate - 1 heartbeatTimer.timeout.connect(heartbeatProvider) heartbeatTimer.start(1000)
In general characteristic and descriptor value updates on the peripheral device use the same methods as connecting Bluetooth Low Energy devices.
Note
To use QtBluetooth (in both central and peripheral roles) on iOS, you have to provide an Info.plist file containing the usage description. According to the CoreBluetooth’s documentation: Your app will crash if its Info.plist doesn’t include usage description keys for the types of data it needs to access. To access Core Bluetooth APIs on apps linked on or after iOS 13, include the NSBluetoothAlwaysUsageDescription key. In iOS 12 and earlier, include NSBluetoothPeripheralUsageDescription to access Bluetooth peripheral data.