MyTetra Share
Делитесь знаниями!
О, смотри-ка какое хорошее место. Дайте два!
Использование Qt Bluetooth Low Energy API (BLE)
06.05.2018
14:09
Текстовые метки: qt, bluetooth, low energy, ble
Раздел: Компьютер - Программирование - Язык C++ - Библиотека Qt - Bluetooth

Здесь описывается использование Bluetooth Low Energy API provided в Qt. На стороне клиента API позволяет создавать подключения к периферийным устройствам, обнаруживать их службы, а также считывать и записывать данные, хранящиеся на устройстве. На стороне сервера API позволяет настраивать службы, рекламировать их и получать уведомления, когда клиент записывает характеристики. Примеры кода, приведенные ниже, берутся из примеров сервера Heart Rate Game и Heart Rate Server.



Установка соединения



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 QBluetoothDeviceDiscoveryAgent::deviceDiscovered() signal and start the search with start():


m_deviceDiscoveryAgent = new QBluetoothDeviceDiscoveryAgent(this);

m_deviceDiscoveryAgent->setLowEnergyDiscoveryTimeout(5000);


connect(m_deviceDiscoveryAgent, &QBluetoothDeviceDiscoveryAgent::deviceDiscovered, this, &DeviceFinder::addDevice);

connect(m_deviceDiscoveryAgent, static_cast<void (QBluetoothDeviceDiscoveryAgent::*)(QBluetoothDeviceDiscoveryAgent::Error)>(&QBluetoothDeviceDiscoveryAgent::error),

this, &DeviceFinder::scanError);


connect(m_deviceDiscoveryAgent, &QBluetoothDeviceDiscoveryAgent::finished, this, &DeviceFinder::scanFinished);

connect(m_deviceDiscoveryAgent, &QBluetoothDeviceDiscoveryAgent::canceled, this, &DeviceFinder::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 QBluetoothDeviceInfo::coreConfigurations() flag:


void DeviceFinder::addDevice(const QBluetoothDeviceInfo &device)

{

// If device is LowEnergy-device, add it to the list

if (device.coreConfigurations() & QBluetoothDeviceInfo::LowEnergyCoreConfiguration) {

m_devices.append(new DeviceInfo(device));

setInfo(tr("Low Energy device found. Scanning more..."));

}

//...

}


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 = new QLowEnergyController(m_currentDevice->getDevice(), this);

connect(m_control, &QLowEnergyController::serviceDiscovered,

this, &DeviceHandler::serviceDiscovered);

connect(m_control, &QLowEnergyController::discoveryFinished,

this, &DeviceHandler::serviceScanDone);


connect(m_control, static_cast<void (QLowEnergyController::*)(QLowEnergyController::Error)>(&QLowEnergyController::error),

this, [this](QLowEnergyController::Error error) {

Q_UNUSED(error);

setError("Cannot connect to remote device.");

});

connect(m_control, &QLowEnergyController::connected, this, [this]() {

setInfo("Controller connected. Search services...");

m_control->discoverServices();

});

connect(m_control, &QLowEnergyController::disconnected, this, [this]() {

setError("LowEnergy controller disconnected");

});


// Connect

m_control->connectToDevice();

Service Search

The above code snippet how the application initiates the service discovery once the connection has been established.

The serviceDiscovered() slot below is triggered as a result of the QLowEnergyController::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 QBluetoothUuid::HeartRate.


void DeviceHandler::serviceDiscovered(const QBluetoothUuid &gatt)

{

if (gatt == QBluetoothUuid(QBluetoothUuid::HeartRate)) {

setInfo("Heart Rate service discovered. Waiting for service scan to be done...");

m_foundHeartRateService = true;

}

}


Eventually the QLowEnergyController::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 QLowEnergyService::discoverDetails():


// If heartRateService found, create new service

if (m_foundHeartRateService)

m_service = m_control->createServiceObject(QBluetoothUuid(QBluetoothUuid::HeartRate), this);


if (m_service) {

connect(m_service, &QLowEnergyService::stateChanged, this, &DeviceHandler::serviceStateChanged);

connect(m_service, &QLowEnergyService::characteristicChanged, this, &DeviceHandler::updateHeartRateValue);

connect(m_service, &QLowEnergyService::descriptorWritten, this, &DeviceHandler::confirmedDescriptorWrite);

m_service->discoverDetails();

} else {

setError("Heart Rate Service not found.");

}


During the detail search the service's state() transitions from DiscoveryRequired to DiscoveringServices and eventually ends with ServiceDiscovered:


void DeviceHandler::serviceStateChanged(QLowEnergyService::ServiceState s)

{

switch (s) {

case QLowEnergyService::DiscoveringServices:

setInfo(tr("Discovering services..."));

break;

case QLowEnergyService::ServiceDiscovered:

{

setInfo(tr("Service discovered."));


const QLowEnergyCharacteristic hrChar = m_service->characteristic(QBluetoothUuid(QBluetoothUuid::HeartRateMeasurement));

if (!hrChar.isValid()) {

setError("HR Data not found.");

break;

}


m_notificationDesc = hrChar.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration);

if (m_notificationDesc.isValid())

m_service->writeDescriptor(m_notificationDesc, QByteArray::fromHex("0100"));


break;

}

default:

//nothing for now

break;

}


emit aliveChanged();

}

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 QLowEnergyCharacteristic::properties() must have the QLowEnergyCharacteristic::Notify flag set and a descriptor of type QBluetoothUuid::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:


void DeviceHandler::updateHeartRateValue(const QLowEnergyCharacteristic &c, const QByteArray &value)

{

// ignore any other characteristic change -> shouldn't really happen though

if (c.uuid() != QBluetoothUuid(QBluetoothUuid::HeartRateMeasurement))

return;


const quint8 *data = reinterpret_cast<const quint8 *>(value.constData());

quint8 flags = data[0];


//Heart Rate

int 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:


QLowEnergyAdvertisingData advertisingData;

advertisingData.setDiscoverability(QLowEnergyAdvertisingData::DiscoverabilityGeneral);

advertisingData.setIncludePowerLevel(true);

advertisingData.setLocalName("HeartRateServer");

advertisingData.setServices(QList<QBluetoothUuid>() << QBluetoothUuid::HeartRate);

const QScopedPointer<QLowEnergyController> leController(QLowEnergyController::createPeripheral());

const QScopedPointer<QLowEnergyService> service(leController->addService(serviceData));

leController->startAdvertising(QLowEnergyAdvertisingParameters(), advertisingData,

advertisingData);


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.


QLowEnergyCharacteristicData charData;

charData.setUuid(QBluetoothUuid::HeartRateMeasurement);

charData.setValue(QByteArray(2, 0));

charData.setProperties(QLowEnergyCharacteristic::Notify);

const QLowEnergyDescriptorData clientConfig(QBluetoothUuid::ClientCharacteristicConfiguration,

QByteArray(2, 0));

charData.addDescriptor(clientConfig);


QLowEnergyServiceData serviceData;

serviceData.setType(QLowEnergyServiceData::ServiceTypePrimary);

serviceData.setUuid(QBluetoothUuid::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.


QTimer heartbeatTimer;

quint8 currentHeartRate = 60;

enum ValueChange { ValueUp, ValueDown } valueChange = ValueUp;

const auto heartbeatProvider = [&service, &currentHeartRate, &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::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);


In general characteristic and descriptor value updates on the peripheral device use the same methods as connecting Bluetooth Low Energy devices.

 
MyTetra Share v.0.52
Яндекс индекс цитирования