blob: ced696858472b63286626fb9f425b1f64162e86c [file] [log] [blame]
/****************************************************************************
**
** Copyright (C) 2018 The Qt Company Ltd.
** Copyright (C) 2014 Denis Shienkov <denis.shienkov@gmail.com>
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtBluetooth module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/
#include "qlowenergycontroller_win_p.h"
#include "qbluetoothdevicediscoveryagent_p.h"
#include <QtCore/QLoggingCategory>
#include <QtCore/QIODevice> // for open modes
#include <QtCore/QEvent>
#include <QtCore/QMutex>
#include <QtCore/QThread>
#include <QtCore/QDataStream>
#include <QtCore/QCoreApplication>
#include <algorithm> // for std::max
#include <SetupAPI.h>
QT_BEGIN_NAMESPACE
Q_DECLARE_LOGGING_CATEGORY(QT_BT_WINDOWS)
Q_GLOBAL_STATIC(QLibrary, bluetoothapis)
Q_GLOBAL_STATIC(QVector<QLowEnergyControllerPrivateWin32 *>, qControllers)
static QMutex controllersGuard(QMutex::NonRecursive);
const QEvent::Type CharactericticValueEventType = static_cast<QEvent::Type>(QEvent::User + 1);
class CharactericticValueEvent : public QEvent
{
public:
explicit CharactericticValueEvent(const PBLUETOOTH_GATT_VALUE_CHANGED_EVENT gattValueChangedEvent)
: QEvent(CharactericticValueEventType)
, m_handle(0)
{
if (!gattValueChangedEvent || gattValueChangedEvent->CharacteristicValueDataSize == 0)
return;
m_handle = gattValueChangedEvent->ChangedAttributeHandle;
const PBTH_LE_GATT_CHARACTERISTIC_VALUE gattValue = gattValueChangedEvent->CharacteristicValue;
if (!gattValue)
return;
m_value = QByteArray(reinterpret_cast<const char *>(&gattValue->Data[0]),
int(gattValue->DataSize));
}
QByteArray m_value;
QLowEnergyHandle m_handle;
};
// Bit masks of ClientCharacteristicConfiguration value, see btle spec.
namespace ClientCharacteristicConfigurationValue {
enum { UseNotifications = 0x1, UseIndications = 0x2 };
}
static bool gattFunctionsResolved = false;
static QBluetoothAddress getDeviceAddress(const QString &servicePath)
{
const int firstbound = servicePath.lastIndexOf(QStringLiteral("_"));
const int lastbound = servicePath.indexOf(QLatin1Char('#'), firstbound);
const QString hex = servicePath.mid(firstbound + 1, lastbound - firstbound - 1);
bool ok = false;
return QBluetoothAddress(hex.toULongLong(&ok, 16));
}
static QString getServiceSystemPath(const QBluetoothAddress &deviceAddress,
const QBluetoothUuid &serviceUuid, int *systemErrorCode)
{
const HDEVINFO deviceInfoSet = ::SetupDiGetClassDevs(
reinterpret_cast<const GUID *>(&serviceUuid),
nullptr,
nullptr,
DIGCF_PRESENT | DIGCF_DEVICEINTERFACE);
if (deviceInfoSet == INVALID_HANDLE_VALUE) {
*systemErrorCode = int(::GetLastError());
return QString();
}
QString foundSystemPath;
DWORD index = 0;
for (;;) {
SP_DEVICE_INTERFACE_DATA deviceInterfaceData;
::ZeroMemory(&deviceInterfaceData, sizeof(deviceInterfaceData));
deviceInterfaceData.cbSize = sizeof(deviceInterfaceData);
if (!::SetupDiEnumDeviceInterfaces(
deviceInfoSet,
nullptr,
reinterpret_cast<const GUID *>(&serviceUuid),
index++,
&deviceInterfaceData)) {
*systemErrorCode = int(::GetLastError());
break;
}
DWORD deviceInterfaceDetailDataSize = 0;
if (!::SetupDiGetDeviceInterfaceDetail(
deviceInfoSet,
&deviceInterfaceData,
nullptr,
deviceInterfaceDetailDataSize,
&deviceInterfaceDetailDataSize,
nullptr)) {
const int error = int(::GetLastError());
if (error != ERROR_INSUFFICIENT_BUFFER) {
*systemErrorCode = error;
break;
}
}
SP_DEVINFO_DATA deviceInfoData;
::ZeroMemory(&deviceInfoData, sizeof(deviceInfoData));
deviceInfoData.cbSize = sizeof(deviceInfoData);
QByteArray deviceInterfaceDetailDataBuffer(
int(deviceInterfaceDetailDataSize), 0);
PSP_INTERFACE_DEVICE_DETAIL_DATA deviceInterfaceDetailData =
reinterpret_cast<PSP_INTERFACE_DEVICE_DETAIL_DATA>
(deviceInterfaceDetailDataBuffer.data());
deviceInterfaceDetailData->cbSize =
sizeof(SP_INTERFACE_DEVICE_DETAIL_DATA);
if (!::SetupDiGetDeviceInterfaceDetail(
deviceInfoSet,
&deviceInterfaceData,
deviceInterfaceDetailData,
DWORD(deviceInterfaceDetailDataBuffer.size()),
&deviceInterfaceDetailDataSize,
&deviceInfoData)) {
*systemErrorCode = int(::GetLastError());
break;
}
// We need to check on required device address which contains in a
// system path. As it is not enough to use only service UUID for this.
const auto candidateSystemPath = QString::fromWCharArray(deviceInterfaceDetailData->DevicePath);
const auto candidateDeviceAddress = getDeviceAddress(candidateSystemPath);
if (candidateDeviceAddress == deviceAddress) {
foundSystemPath = candidateSystemPath;
*systemErrorCode = NO_ERROR;
break;
}
}
::SetupDiDestroyDeviceInfoList(deviceInfoSet);
return foundSystemPath;
}
static HANDLE openSystemDevice(
const QString &systemPath, QIODevice::OpenMode openMode, int *systemErrorCode)
{
DWORD desiredAccess = 0;
DWORD shareMode = FILE_SHARE_READ | FILE_SHARE_WRITE;
if (openMode & QIODevice::ReadOnly) {
desiredAccess |= GENERIC_READ;
}
if (openMode & QIODevice::WriteOnly) {
desiredAccess |= GENERIC_WRITE;
shareMode &= ~DWORD(FILE_SHARE_WRITE);
}
const HANDLE hDevice = ::CreateFile(
reinterpret_cast<const wchar_t *>(systemPath.utf16()),
desiredAccess,
shareMode,
nullptr,
OPEN_EXISTING,
0,
nullptr);
*systemErrorCode = (INVALID_HANDLE_VALUE == hDevice)
? int(::GetLastError()) : NO_ERROR;
return hDevice;
}
static HANDLE openSystemService(const QBluetoothAddress &deviceAddress,
const QBluetoothUuid &service, QIODevice::OpenMode openMode, int *systemErrorCode)
{
const QString serviceSystemPath = getServiceSystemPath(
deviceAddress, service, systemErrorCode);
if (*systemErrorCode != NO_ERROR)
return INVALID_HANDLE_VALUE;
const HANDLE hService = openSystemDevice(
serviceSystemPath, openMode, systemErrorCode);
if (*systemErrorCode != NO_ERROR)
return INVALID_HANDLE_VALUE;
return hService;
}
static void closeSystemDevice(HANDLE hDevice)
{
if (hDevice && hDevice != INVALID_HANDLE_VALUE)
::CloseHandle(hDevice);
}
static QVector<BTH_LE_GATT_SERVICE> enumeratePrimaryGattServices(
HANDLE hDevice, int *systemErrorCode)
{
if (!gattFunctionsResolved) {
*systemErrorCode = ERROR_NOT_SUPPORTED;
return QVector<BTH_LE_GATT_SERVICE>();
}
QVector<BTH_LE_GATT_SERVICE> foundServices;
USHORT servicesCount = 0;
for (;;) {
const HRESULT hr = ::BluetoothGATTGetServices(
hDevice,
servicesCount,
foundServices.isEmpty() ? nullptr : &foundServices[0],
&servicesCount,
BLUETOOTH_GATT_FLAG_NONE);
if (SUCCEEDED(hr)) {
*systemErrorCode = NO_ERROR;
return foundServices;
} else {
const int error = WIN32_FROM_HRESULT(hr);
if (error == ERROR_MORE_DATA) {
foundServices.resize(servicesCount);
} else {
*systemErrorCode = error;
return QVector<BTH_LE_GATT_SERVICE>();
}
}
}
}
static QVector<BTH_LE_GATT_CHARACTERISTIC> enumerateGattCharacteristics(
HANDLE hService, PBTH_LE_GATT_SERVICE gattService, int *systemErrorCode)
{
if (!gattFunctionsResolved) {
*systemErrorCode = ERROR_NOT_SUPPORTED;
return QVector<BTH_LE_GATT_CHARACTERISTIC>();
}
QVector<BTH_LE_GATT_CHARACTERISTIC> foundCharacteristics;
USHORT characteristicsCount = 0;
for (;;) {
const HRESULT hr = ::BluetoothGATTGetCharacteristics(
hService,
gattService,
characteristicsCount,
foundCharacteristics.isEmpty() ? nullptr : &foundCharacteristics[0],
&characteristicsCount,
BLUETOOTH_GATT_FLAG_NONE);
if (SUCCEEDED(hr)) {
*systemErrorCode = NO_ERROR;
return foundCharacteristics;
} else {
const int error = WIN32_FROM_HRESULT(hr);
if (error == ERROR_MORE_DATA) {
foundCharacteristics.resize(characteristicsCount);
} else {
*systemErrorCode = error;
return QVector<BTH_LE_GATT_CHARACTERISTIC>();
}
}
}
}
static QByteArray getGattCharacteristicValue(
HANDLE hService, PBTH_LE_GATT_CHARACTERISTIC gattCharacteristic, int *systemErrorCode)
{
if (!gattFunctionsResolved) {
*systemErrorCode = ERROR_NOT_SUPPORTED;
return QByteArray();
}
QByteArray valueBuffer;
USHORT valueBufferSize = 0;
for (;;) {
const auto valuePtr = valueBuffer.isEmpty()
? nullptr
: reinterpret_cast<PBTH_LE_GATT_CHARACTERISTIC_VALUE>(valueBuffer.data());
const HRESULT hr = ::BluetoothGATTGetCharacteristicValue(
hService,
gattCharacteristic,
valueBufferSize,
valuePtr,
&valueBufferSize,
BLUETOOTH_GATT_FLAG_NONE);
if (SUCCEEDED(hr)) {
*systemErrorCode = NO_ERROR;
return QByteArray(reinterpret_cast<const char *>(&valuePtr->Data[0]),
int(valuePtr->DataSize));
} else {
const int error = WIN32_FROM_HRESULT(hr);
if (error == ERROR_MORE_DATA) {
valueBuffer.resize(valueBufferSize);
valueBuffer.fill(0);
} else {
*systemErrorCode = error;
return QByteArray();
}
}
}
}
static void setGattCharacteristicValue(
HANDLE hService, PBTH_LE_GATT_CHARACTERISTIC gattCharacteristic,
const QByteArray &value, DWORD flags, int *systemErrorCode)
{
if (!gattFunctionsResolved) {
*systemErrorCode = ERROR_NOT_SUPPORTED;
return;
}
QByteArray valueBuffer;
QDataStream out(&valueBuffer, QIODevice::WriteOnly);
ULONG dataSize = ULONG(value.size());
out.writeRawData(reinterpret_cast<const char *>(&dataSize), sizeof(dataSize));
out.writeRawData(value.constData(), value.size());
BTH_LE_GATT_RELIABLE_WRITE_CONTEXT reliableWriteContext = 0;
const HRESULT hr = ::BluetoothGATTSetCharacteristicValue(
hService,
gattCharacteristic,
reinterpret_cast<PBTH_LE_GATT_CHARACTERISTIC_VALUE>(valueBuffer.data()),
reliableWriteContext,
flags);
if (SUCCEEDED(hr))
*systemErrorCode = NO_ERROR;
else
*systemErrorCode = WIN32_FROM_HRESULT(hr);
}
static QVector<BTH_LE_GATT_DESCRIPTOR> enumerateGattDescriptors(
HANDLE hService, PBTH_LE_GATT_CHARACTERISTIC gattCharacteristic, int *systemErrorCode)
{
if (!gattFunctionsResolved) {
*systemErrorCode = ERROR_NOT_SUPPORTED;
return QVector<BTH_LE_GATT_DESCRIPTOR>();
}
QVector<BTH_LE_GATT_DESCRIPTOR> foundDescriptors;
USHORT descriptorsCount = 0;
for (;;) {
const HRESULT hr = ::BluetoothGATTGetDescriptors(
hService,
gattCharacteristic,
descriptorsCount,
foundDescriptors.isEmpty() ? nullptr : &foundDescriptors[0],
&descriptorsCount,
BLUETOOTH_GATT_FLAG_NONE);
if (SUCCEEDED(hr)) {
*systemErrorCode = NO_ERROR;
return foundDescriptors;
} else {
const int error = WIN32_FROM_HRESULT(hr);
if (error == ERROR_MORE_DATA) {
foundDescriptors.resize(descriptorsCount);
} else {
*systemErrorCode = error;
return QVector<BTH_LE_GATT_DESCRIPTOR>();
}
}
}
}
static QByteArray getGattDescriptorValue(
HANDLE hService, PBTH_LE_GATT_DESCRIPTOR gattDescriptor, int *systemErrorCode)
{
if (!gattFunctionsResolved) {
*systemErrorCode = ERROR_NOT_SUPPORTED;
return QByteArray();
}
QByteArray valueBuffer;
USHORT valueBufferSize = 0;
for (;;) {
const auto valuePtr = valueBuffer.isEmpty()
? nullptr
: reinterpret_cast<PBTH_LE_GATT_DESCRIPTOR_VALUE>(valueBuffer.data());
const HRESULT hr = ::BluetoothGATTGetDescriptorValue(
hService,
gattDescriptor,
valueBufferSize,
valuePtr,
&valueBufferSize,
BLUETOOTH_GATT_FLAG_NONE);
if (SUCCEEDED(hr)) {
*systemErrorCode = NO_ERROR;
if (gattDescriptor->DescriptorType == CharacteristicUserDescription) {
QString valueString = QString::fromUtf16(reinterpret_cast<const ushort *>(&valuePtr->Data[0]),
valuePtr->DataSize/2);
return valueString.toUtf8();
}
return QByteArray(reinterpret_cast<const char *>(&valuePtr->Data[0]),
int(valuePtr->DataSize));
} else {
const int error = WIN32_FROM_HRESULT(hr);
if (error == ERROR_MORE_DATA) {
valueBuffer.resize(valueBufferSize);
valueBuffer.fill(0);
} else {
*systemErrorCode = error;
return QByteArray();
}
}
}
}
static void setGattDescriptorValue(
HANDLE hService, PBTH_LE_GATT_DESCRIPTOR gattDescriptor,
QByteArray value, int *systemErrorCode)
{
if (!gattFunctionsResolved) {
*systemErrorCode = ERROR_NOT_SUPPORTED;
return;
}
const int requiredValueBufferSize = int(sizeof(BTH_LE_GATT_DESCRIPTOR_VALUE))
+ value.size();
QByteArray valueBuffer(requiredValueBufferSize, 0);
PBTH_LE_GATT_DESCRIPTOR_VALUE gattValue = reinterpret_cast<
PBTH_LE_GATT_DESCRIPTOR_VALUE>(valueBuffer.data());
gattValue->DescriptorType = gattDescriptor->DescriptorType;
if (gattValue->DescriptorType == ClientCharacteristicConfiguration) {
QDataStream in(value);
quint8 u;
in >> u;
// We need to setup appropriate fields that allow to subscribe for events.
gattValue->ClientCharacteristicConfiguration.IsSubscribeToNotification =
bool(u & ClientCharacteristicConfigurationValue::UseNotifications);
gattValue->ClientCharacteristicConfiguration.IsSubscribeToIndication =
bool(u & ClientCharacteristicConfigurationValue::UseIndications);
}
gattValue->DataSize = ULONG(value.size());
::memcpy(gattValue->Data, value.constData(), size_t(value.size()));
const HRESULT hr = ::BluetoothGATTSetDescriptorValue(
hService,
gattDescriptor,
gattValue,
BLUETOOTH_GATT_FLAG_NONE);
if (SUCCEEDED(hr))
*systemErrorCode = NO_ERROR;
else
*systemErrorCode = WIN32_FROM_HRESULT(hr);
}
static void WINAPI eventChangedCallbackEntry(
BTH_LE_GATT_EVENT_TYPE eventType, PVOID eventOutParameter, PVOID context)
{
if ((eventType != CharacteristicValueChangedEvent) || !eventOutParameter || !context)
return;
QMutexLocker locker(&controllersGuard);
const auto target = static_cast<QLowEnergyControllerPrivateWin32 *>(context);
if (!qControllers->contains(target))
return;
CharactericticValueEvent *e = new CharactericticValueEvent(
reinterpret_cast<const PBLUETOOTH_GATT_VALUE_CHANGED_EVENT>(eventOutParameter));
QCoreApplication::postEvent(target, e);
}
static HANDLE registerEvent(
HANDLE hService, BTH_LE_GATT_CHARACTERISTIC gattCharacteristic,
PVOID context, int *systemErrorCode)
{
if (!gattFunctionsResolved) {
*systemErrorCode = ERROR_NOT_SUPPORTED;
return INVALID_HANDLE_VALUE;
}
HANDLE hEvent = INVALID_HANDLE_VALUE;
BLUETOOTH_GATT_VALUE_CHANGED_EVENT_REGISTRATION registration;
::ZeroMemory(&registration, sizeof(registration));
registration.NumCharacteristics = 1;
registration.Characteristics[0] = gattCharacteristic;
const HRESULT hr = ::BluetoothGATTRegisterEvent(
hService,
CharacteristicValueChangedEvent,
&registration,
eventChangedCallbackEntry,
context,
&hEvent,
BLUETOOTH_GATT_FLAG_NONE);
if (SUCCEEDED(hr))
*systemErrorCode = NO_ERROR;
else
*systemErrorCode = WIN32_FROM_HRESULT(hr);
return hEvent;
}
static void unregisterEvent(HANDLE hEvent, int *systemErrorCode)
{
if (!gattFunctionsResolved) {
*systemErrorCode = ERROR_NOT_SUPPORTED;
return;
}
const HRESULT hr = ::BluetoothGATTUnregisterEvent(
hEvent,
BLUETOOTH_GATT_FLAG_NONE);
if (SUCCEEDED(hr))
*systemErrorCode = NO_ERROR;
else
*systemErrorCode = WIN32_FROM_HRESULT(hr);
}
static QBluetoothUuid qtBluetoothUuidFromNativeLeUuid(const BTH_LE_UUID &uuid)
{
return uuid.IsShortUuid ? QBluetoothUuid(uuid.Value.ShortUuid)
: QBluetoothUuid(uuid.Value.LongUuid);
}
static BTH_LE_UUID nativeLeUuidFromQtBluetoothUuid(const QBluetoothUuid &uuid)
{
BTH_LE_UUID gattUuid;
::ZeroMemory(&gattUuid, sizeof(gattUuid));
if (uuid.minimumSize() == 2) {
gattUuid.IsShortUuid = TRUE;
gattUuid.Value.ShortUuid = USHORT(uuid.data1); // other fields should be empty!
} else {
gattUuid.Value.LongUuid = uuid;
}
return gattUuid;
}
static BTH_LE_GATT_CHARACTERISTIC recoverNativeLeGattCharacteristic(
QLowEnergyHandle serviceHandle, QLowEnergyHandle characteristicHandle,
const QLowEnergyServicePrivate::CharData &characteristicData)
{
BTH_LE_GATT_CHARACTERISTIC gattCharacteristic;
gattCharacteristic.ServiceHandle = serviceHandle;
gattCharacteristic.AttributeHandle = characteristicHandle;
gattCharacteristic.CharacteristicValueHandle = characteristicData.valueHandle;
gattCharacteristic.CharacteristicUuid = nativeLeUuidFromQtBluetoothUuid(
characteristicData.uuid);
gattCharacteristic.HasExtendedProperties = bool(characteristicData.properties
& QLowEnergyCharacteristic::ExtendedProperty);
gattCharacteristic.IsBroadcastable = bool(characteristicData.properties
& QLowEnergyCharacteristic::Broadcasting);
gattCharacteristic.IsIndicatable = bool(characteristicData.properties
& QLowEnergyCharacteristic::Indicate);
gattCharacteristic.IsNotifiable = bool(characteristicData.properties
& QLowEnergyCharacteristic::Notify);
gattCharacteristic.IsReadable = bool(characteristicData.properties
& QLowEnergyCharacteristic::Read);
gattCharacteristic.IsSignedWritable = bool(characteristicData.properties
& QLowEnergyCharacteristic::WriteSigned);
gattCharacteristic.IsWritable = bool(characteristicData.properties
& QLowEnergyCharacteristic::Write);
gattCharacteristic.IsWritableWithoutResponse = bool(characteristicData.properties
& QLowEnergyCharacteristic::WriteNoResponse);
return gattCharacteristic;
}
static BTH_LE_GATT_DESCRIPTOR_TYPE nativeLeGattDescriptorTypeFromUuid(
const QBluetoothUuid &uuid)
{
switch (uuid.toUInt16()) {
case QBluetoothUuid::CharacteristicExtendedProperties:
return CharacteristicExtendedProperties;
case QBluetoothUuid::CharacteristicUserDescription:
return CharacteristicUserDescription;
case QBluetoothUuid::ClientCharacteristicConfiguration:
return ClientCharacteristicConfiguration;
case QBluetoothUuid::ServerCharacteristicConfiguration:
return ServerCharacteristicConfiguration;
case QBluetoothUuid::CharacteristicPresentationFormat:
return CharacteristicFormat;
case QBluetoothUuid::CharacteristicAggregateFormat:
return CharacteristicAggregateFormat;
default:
return CustomDescriptor;
}
}
static BTH_LE_GATT_DESCRIPTOR recoverNativeLeGattDescriptor(
QLowEnergyHandle serviceHandle, QLowEnergyHandle characteristicHandle,
QLowEnergyHandle descriptorHandle,
const QLowEnergyServicePrivate::DescData &descriptorData)
{
BTH_LE_GATT_DESCRIPTOR gattDescriptor;
gattDescriptor.ServiceHandle = serviceHandle;
gattDescriptor.CharacteristicHandle = characteristicHandle;
gattDescriptor.AttributeHandle = descriptorHandle;
gattDescriptor.DescriptorUuid = nativeLeUuidFromQtBluetoothUuid(
descriptorData.uuid);
gattDescriptor.DescriptorType = nativeLeGattDescriptorTypeFromUuid
(descriptorData.uuid);
return gattDescriptor;
}
void QLowEnergyControllerPrivateWin32::customEvent(QEvent *e)
{
if (e->type() != CharactericticValueEventType)
return;
const CharactericticValueEvent *characteristicEvent
= static_cast<CharactericticValueEvent *>(e);
updateValueOfCharacteristic(characteristicEvent->m_handle,
characteristicEvent->m_value, false);
const QSharedPointer<QLowEnergyServicePrivate> service = serviceForHandle(
characteristicEvent->m_handle);
if (service.isNull())
return;
const QLowEnergyCharacteristic ch(service, characteristicEvent->m_handle);
emit service->characteristicChanged(ch, characteristicEvent->m_value);
}
QLowEnergyControllerPrivateWin32::QLowEnergyControllerPrivateWin32()
: QLowEnergyControllerPrivate()
{
QMutexLocker locker(&controllersGuard);
qControllers()->append(this);
gattFunctionsResolved = resolveFunctions(bluetoothapis());
if (!gattFunctionsResolved) {
qCWarning(QT_BT_WINDOWS) << "LE is not supported on this OS";
return;
}
}
QLowEnergyControllerPrivateWin32::~QLowEnergyControllerPrivateWin32()
{
QMutexLocker locker(&controllersGuard);
qControllers()->removeAll(this);
}
void QLowEnergyControllerPrivateWin32::init()
{
}
void QLowEnergyControllerPrivateWin32::connectToDevice()
{
// required to pass unit test on default backend
if (remoteDevice.isNull()) {
qWarning() << "Invalid/null remote device address";
setError(QLowEnergyController::UnknownRemoteDeviceError);
return;
}
if (!deviceSystemPath.isEmpty()) {
qCDebug(QT_BT_WINDOWS) << "Already is connected";
return;
}
setState(QLowEnergyController::ConnectingState);
deviceSystemPath =
QBluetoothDeviceDiscoveryAgentPrivate::discoveredLeDeviceSystemPath(
remoteDevice);
if (deviceSystemPath.isEmpty()) {
qCWarning(QT_BT_WINDOWS) << qt_error_string(ERROR_PATH_NOT_FOUND);
setError(QLowEnergyController::UnknownRemoteDeviceError);
setState(QLowEnergyController::UnconnectedState);
return;
}
setState(QLowEnergyController::ConnectedState);
thread = new QThread;
threadWorker = new ThreadWorker;
threadWorker->moveToThread(thread);
connect(threadWorker, &ThreadWorker::jobFinished, this, &QLowEnergyControllerPrivateWin32::jobFinished);
connect(thread, &QThread::finished, threadWorker, &ThreadWorker::deleteLater);
connect(thread, &QThread::finished, thread, &QThread::deleteLater);
thread->start();
Q_Q(QLowEnergyController);
emit q->connected();
}
void QLowEnergyControllerPrivateWin32::disconnectFromDevice()
{
if (deviceSystemPath.isEmpty()) {
qCDebug(QT_BT_WINDOWS) << "Already is disconnected";
return;
}
setState(QLowEnergyController::ClosingState);
deviceSystemPath.clear();
setState(QLowEnergyController::UnconnectedState);
if (thread) {
disconnect(threadWorker, &ThreadWorker::jobFinished, this, &QLowEnergyControllerPrivateWin32::jobFinished);
thread->quit();
thread = nullptr;
}
for (const auto &servicePrivate: serviceList)
closeSystemDevice(servicePrivate->hService);
Q_Q(QLowEnergyController);
emit q->disconnected();
}
void QLowEnergyControllerPrivateWin32::discoverServices()
{
int systemErrorCode = NO_ERROR;
const HANDLE hDevice = openSystemDevice(
deviceSystemPath, QIODevice::ReadOnly, &systemErrorCode);
if (systemErrorCode != NO_ERROR) {
qCWarning(QT_BT_WINDOWS) << qt_error_string(systemErrorCode);
setError(QLowEnergyController::NetworkError);
setState(QLowEnergyController::ConnectedState);
return;
}
const QVector<BTH_LE_GATT_SERVICE> foundServices =
enumeratePrimaryGattServices(hDevice, &systemErrorCode);
closeSystemDevice(hDevice);
if (systemErrorCode != NO_ERROR) {
qCWarning(QT_BT_WINDOWS) << qt_error_string(systemErrorCode);
setError(QLowEnergyController::NetworkError);
setState(QLowEnergyController::ConnectedState);
return;
}
setState(QLowEnergyController::DiscoveringState);
Q_Q(QLowEnergyController);
for (const BTH_LE_GATT_SERVICE &service : foundServices) {
const QBluetoothUuid uuid = qtBluetoothUuidFromNativeLeUuid(
service.ServiceUuid);
qCDebug(QT_BT_WINDOWS) << "Found uuid:" << uuid;
QLowEnergyServicePrivate *priv = new QLowEnergyServicePrivate();
priv->uuid = uuid;
priv->type = QLowEnergyService::PrimaryService;
priv->startHandle = service.AttributeHandle;
priv->setController(this);
QSharedPointer<QLowEnergyServicePrivate> pointer(priv);
serviceList.insert(uuid, pointer);
emit q->serviceDiscovered(uuid);
}
setState(QLowEnergyController::DiscoveredState);
emit q->discoveryFinished();
}
void QLowEnergyControllerPrivateWin32::discoverServiceDetails(
const QBluetoothUuid &service)
{
if (!serviceList.contains(service)) {
qCWarning(QT_BT_WINDOWS) << "Discovery of unknown service" << service.toString()
<< "not possible";
return;
}
const QSharedPointer<QLowEnergyServicePrivate> servicePrivate =
serviceList.value(service);
int systemErrorCode = NO_ERROR;
// Only open a service once and close it in the QLowEnergyServicePrivate destructor
if (!servicePrivate->hService || servicePrivate->hService == INVALID_HANDLE_VALUE) {
servicePrivate->hService = openSystemService(remoteDevice, service,
QIODevice::ReadOnly | QIODevice::WriteOnly,
&systemErrorCode);
if (systemErrorCode != NO_ERROR) {
servicePrivate->hService = openSystemService(remoteDevice, service,
QIODevice::ReadOnly,
&systemErrorCode);
}
}
if (systemErrorCode != NO_ERROR) {
qCWarning(QT_BT_WINDOWS) << "Unable to open service" << service.toString()
<< ":" << qt_error_string(systemErrorCode);
servicePrivate->setError(QLowEnergyService::UnknownError);
servicePrivate->setState(QLowEnergyService::DiscoveryRequired);
return;
}
// We assume that the service does not have any characteristics with descriptors.
servicePrivate->endHandle = servicePrivate->startHandle;
const QVector<BTH_LE_GATT_CHARACTERISTIC> foundCharacteristics =
enumerateGattCharacteristics(servicePrivate->hService, nullptr, &systemErrorCode);
if (systemErrorCode != NO_ERROR) {
qCWarning(QT_BT_WINDOWS) << "Unable to get characteristics for service" << service.toString()
<< ":" << qt_error_string(systemErrorCode);
servicePrivate->setError(QLowEnergyService::CharacteristicReadError);
servicePrivate->setState(QLowEnergyService::DiscoveryRequired);
return;
}
for (const BTH_LE_GATT_CHARACTERISTIC &gattCharacteristic : foundCharacteristics) {
const QLowEnergyHandle characteristicHandle = gattCharacteristic.AttributeHandle;
QLowEnergyServicePrivate::CharData detailsData;
detailsData.hValueChangeEvent = nullptr;
detailsData.uuid = qtBluetoothUuidFromNativeLeUuid(
gattCharacteristic.CharacteristicUuid);
detailsData.valueHandle = gattCharacteristic.CharacteristicValueHandle;
QLowEnergyCharacteristic::PropertyTypes properties = QLowEnergyCharacteristic::Unknown;
if (gattCharacteristic.HasExtendedProperties)
properties |= QLowEnergyCharacteristic::ExtendedProperty;
if (gattCharacteristic.IsBroadcastable)
properties |= QLowEnergyCharacteristic::Broadcasting;
if (gattCharacteristic.IsIndicatable)
properties |= QLowEnergyCharacteristic::Indicate;
if (gattCharacteristic.IsNotifiable)
properties |= QLowEnergyCharacteristic::Notify;
if (gattCharacteristic.IsReadable)
properties |= QLowEnergyCharacteristic::Read;
if (gattCharacteristic.IsSignedWritable)
properties |= QLowEnergyCharacteristic::WriteSigned;
if (gattCharacteristic.IsWritable)
properties |= QLowEnergyCharacteristic::Write;
if (gattCharacteristic.IsWritableWithoutResponse)
properties |= QLowEnergyCharacteristic::WriteNoResponse;
detailsData.properties = properties;
detailsData.value = getGattCharacteristicValue(
servicePrivate->hService, const_cast<PBTH_LE_GATT_CHARACTERISTIC>(
&gattCharacteristic), &systemErrorCode);
if (systemErrorCode != NO_ERROR) {
// We do not interrupt enumerating of characteristics
// if value can not be read
qCWarning(QT_BT_WINDOWS) << "Unable to get value for characteristic"
<< detailsData.uuid.toString()
<< "of the service" << service.toString()
<< ":" << qt_error_string(systemErrorCode);
}
// We assume that the characteristic has no any descriptors. So, the
// biggest characteristic + 1 will indicate an end handle of service.
servicePrivate->endHandle = std::max(
servicePrivate->endHandle,
QLowEnergyHandle(gattCharacteristic.AttributeHandle + 1));
const QVector<BTH_LE_GATT_DESCRIPTOR> foundDescriptors = enumerateGattDescriptors(
servicePrivate->hService, const_cast<PBTH_LE_GATT_CHARACTERISTIC>(
&gattCharacteristic), &systemErrorCode);
if (systemErrorCode != NO_ERROR) {
if (systemErrorCode != ERROR_NOT_FOUND) {
qCWarning(QT_BT_WINDOWS) << "Unable to get descriptor for characteristic"
<< detailsData.uuid.toString()
<< "of the service" << service.toString()
<< ":" << qt_error_string(systemErrorCode);
servicePrivate->setError(QLowEnergyService::DescriptorReadError);
servicePrivate->setState(QLowEnergyService::DiscoveryRequired);
return;
}
}
for (const BTH_LE_GATT_DESCRIPTOR &gattDescriptor : foundDescriptors) {
const QLowEnergyHandle descriptorHandle = gattDescriptor.AttributeHandle;
QLowEnergyServicePrivate::DescData data;
data.uuid = qtBluetoothUuidFromNativeLeUuid(
gattDescriptor.DescriptorUuid);
data.value = getGattDescriptorValue(servicePrivate->hService, const_cast<PBTH_LE_GATT_DESCRIPTOR>(
&gattDescriptor), &systemErrorCode);
if (systemErrorCode != NO_ERROR) {
qCWarning(QT_BT_WINDOWS) << "Unable to get value for descriptor"
<< data.uuid.toString()
<< "for characteristic"
<< detailsData.uuid.toString()
<< "of the service" << service.toString()
<< ":" << qt_error_string(systemErrorCode);
servicePrivate->setError(QLowEnergyService::DescriptorReadError);
servicePrivate->setState(QLowEnergyService::DiscoveryRequired);
return;
}
// Biggest descriptor will contain an end handle of service.
servicePrivate->endHandle = std::max(
servicePrivate->endHandle,
QLowEnergyHandle(gattDescriptor.AttributeHandle));
detailsData.descriptorList.insert(descriptorHandle, data);
}
servicePrivate->characteristicList.insert(characteristicHandle, detailsData);
}
servicePrivate->setState(QLowEnergyService::ServiceDiscovered);
}
void QLowEnergyControllerPrivateWin32::startAdvertising(const QLowEnergyAdvertisingParameters &, const QLowEnergyAdvertisingData &, const QLowEnergyAdvertisingData &)
{
Q_UNIMPLEMENTED();
}
void QLowEnergyControllerPrivateWin32::stopAdvertising()
{
Q_UNIMPLEMENTED();
}
void QLowEnergyControllerPrivateWin32::requestConnectionUpdate(const QLowEnergyConnectionParameters &)
{
Q_UNIMPLEMENTED();
}
void QLowEnergyControllerPrivateWin32::readCharacteristic(
const QSharedPointer<QLowEnergyServicePrivate> service,
const QLowEnergyHandle charHandle)
{
Q_ASSERT(!service.isNull());
if (!service->characteristicList.contains(charHandle))
return;
const QLowEnergyServicePrivate::CharData &charDetails
= service->characteristicList[charHandle];
if (!(charDetails.properties & QLowEnergyCharacteristic::Read)) {
// if this succeeds the device has a bug, char is advertised as
// non-readable. We try to be permissive and let the remote
// device answer to the read attempt
qCWarning(QT_BT_WINDOWS) << "Reading non-readable char" << charHandle;
}
ReadCharData data;
data.systemErrorCode = NO_ERROR;
data.hService = service->hService;
if (data.systemErrorCode != NO_ERROR) {
qCWarning(QT_BT_WINDOWS) << "Unable to open service" << service->uuid.toString()
<< ":" << qt_error_string(data.systemErrorCode);
service->setError(QLowEnergyService::CharacteristicReadError);
return;
}
data.gattCharacteristic = recoverNativeLeGattCharacteristic(
service->startHandle, charHandle, charDetails);
ThreadWorkerJob job;
job.operation = ThreadWorkerJob::ReadChar;
job.data = QVariant::fromValue(data);
QMetaObject::invokeMethod(threadWorker, "putJob", Qt::QueuedConnection,
Q_ARG(ThreadWorkerJob, job));
}
void QLowEnergyControllerPrivateWin32::writeCharacteristic(
const QSharedPointer<QLowEnergyServicePrivate> service,
const QLowEnergyHandle charHandle,
const QByteArray &newValue,
QLowEnergyService::WriteMode mode)
{
Q_ASSERT(!service.isNull());
if (!service->characteristicList.contains(charHandle)) {
service->setError(QLowEnergyService::CharacteristicWriteError);
return;
}
WriteCharData data;
data.systemErrorCode = NO_ERROR;
data.hService = service->hService;
if (data.systemErrorCode != NO_ERROR) {
qCWarning(QT_BT_WINDOWS) << "Unable to open service" << service->uuid.toString()
<< ":" << qt_error_string(data.systemErrorCode);
service->setError(QLowEnergyService::CharacteristicWriteError);
return;
}
const QLowEnergyServicePrivate::CharData &charDetails
= service->characteristicList[charHandle];
data.gattCharacteristic = recoverNativeLeGattCharacteristic(
service->startHandle, charHandle, charDetails);
data.flags = (mode == QLowEnergyService::WriteWithResponse)
? BLUETOOTH_GATT_FLAG_NONE
: BLUETOOTH_GATT_FLAG_WRITE_WITHOUT_RESPONSE;
ThreadWorkerJob job;
job.operation = ThreadWorkerJob::WriteChar;
data.newValue = newValue;
data.mode = mode;
job.data = QVariant::fromValue(data);
QMetaObject::invokeMethod(threadWorker, "putJob", Qt::QueuedConnection,
Q_ARG(ThreadWorkerJob, job));
}
void QLowEnergyControllerPrivateWin32::jobFinished(const ThreadWorkerJob &job)
{
switch (job.operation) {
case ThreadWorkerJob::WriteChar:
{
const WriteCharData data = job.data.value<WriteCharData>();
const QLowEnergyHandle charHandle = static_cast<QLowEnergyHandle>(data.gattCharacteristic.AttributeHandle);
const QSharedPointer<QLowEnergyServicePrivate> service = serviceForHandle(charHandle);
if (data.systemErrorCode != NO_ERROR) {
const QLowEnergyServicePrivate::CharData &charDetails = service->characteristicList[charHandle];
qCWarning(QT_BT_WINDOWS) << "Unable to set value for characteristic"
<< charDetails.uuid.toString()
<< "of the service" << service->uuid.toString()
<< ":" << qt_error_string(data.systemErrorCode);
service->setError(QLowEnergyService::CharacteristicWriteError);
return;
}
updateValueOfCharacteristic(charHandle, data.newValue, false);
if (data.mode == QLowEnergyService::WriteWithResponse) {
const QLowEnergyCharacteristic ch = characteristicForHandle(charHandle);
emit service->characteristicWritten(ch, data.newValue);
}
}
break;
case ThreadWorkerJob::ReadChar:
{
const ReadCharData data = job.data.value<ReadCharData>();
const QLowEnergyHandle charHandle = static_cast<QLowEnergyHandle>(data.gattCharacteristic.AttributeHandle);
const QSharedPointer<QLowEnergyServicePrivate> service = serviceForHandle(charHandle);
if (data.systemErrorCode != NO_ERROR) {
const QLowEnergyServicePrivate::CharData &charDetails = service->characteristicList[charHandle];
qCWarning(QT_BT_WINDOWS) << "Unable to get value for characteristic"
<< charDetails.uuid.toString()
<< "of the service" << service->uuid.toString()
<< ":" << qt_error_string(data.systemErrorCode);
service->setError(QLowEnergyService::CharacteristicReadError);
return;
}
updateValueOfCharacteristic(charHandle, data.value, false);
const QLowEnergyCharacteristic ch(service, charHandle);
emit service->characteristicRead(ch, data.value);
}
break;
case ThreadWorkerJob::WriteDescr:
{
WriteDescData data = job.data.value<WriteDescData>();
const QLowEnergyHandle descriptorHandle = static_cast<QLowEnergyHandle>(data.gattDescriptor.AttributeHandle);
const QLowEnergyHandle charHandle = static_cast<QLowEnergyHandle>(data.gattDescriptor.CharacteristicHandle);
const QSharedPointer<QLowEnergyServicePrivate> service = serviceForHandle(charHandle);
QLowEnergyServicePrivate::CharData &charDetails = service->characteristicList[charHandle];
const QLowEnergyServicePrivate::DescData &dscrDetails = charDetails.descriptorList[descriptorHandle];
if (data.systemErrorCode != NO_ERROR) {
qCWarning(QT_BT_WINDOWS) << "Unable to set value for descriptor"
<< dscrDetails.uuid.toString()
<< "for characteristic"
<< charDetails.uuid.toString()
<< "of the service" << service->uuid.toString()
<< ":" << qt_error_string(data.systemErrorCode);
service->setError(QLowEnergyService::DescriptorWriteError);
return;
}
if (data.gattDescriptor.DescriptorType == ClientCharacteristicConfiguration) {
QDataStream in(data.newValue);
quint8 u;
in >> u;
if (u & ClientCharacteristicConfigurationValue::UseNotifications
|| u & ClientCharacteristicConfigurationValue::UseIndications) {
if (!charDetails.hValueChangeEvent) {
BTH_LE_GATT_CHARACTERISTIC gattCharacteristic = recoverNativeLeGattCharacteristic(
service->startHandle, charHandle, charDetails);
// note: if the service handle is closed the event registration is no longer valid.
charDetails.hValueChangeEvent = registerEvent(
data.hService, gattCharacteristic, this, &data.systemErrorCode);
}
} else {
if (charDetails.hValueChangeEvent) {
unregisterEvent(charDetails.hValueChangeEvent, &data.systemErrorCode);
charDetails.hValueChangeEvent = nullptr;
}
}
if (data.systemErrorCode != NO_ERROR) {
qCWarning(QT_BT_WINDOWS) << "Unable to subscribe events for descriptor"
<< dscrDetails.uuid.toString()
<< "for characteristic"
<< charDetails.uuid.toString()
<< "of the service" << service->uuid.toString()
<< ":" << qt_error_string(data.systemErrorCode);
service->setError(QLowEnergyService::DescriptorWriteError);
return;
}
}
updateValueOfDescriptor(charHandle, descriptorHandle, data.newValue, false);
const QLowEnergyDescriptor dscr(service, charHandle, descriptorHandle);
emit service->descriptorWritten(dscr, data.newValue);
}
break;
case ThreadWorkerJob::ReadDescr:
{
ReadDescData data = job.data.value<ReadDescData>();
const QLowEnergyHandle descriptorHandle = static_cast<QLowEnergyHandle>(data.gattDescriptor.AttributeHandle);
const QLowEnergyHandle charHandle = static_cast<QLowEnergyHandle>(data.gattDescriptor.CharacteristicHandle);
const QSharedPointer<QLowEnergyServicePrivate> service = serviceForHandle(charHandle);
QLowEnergyServicePrivate::CharData &charDetails = service->characteristicList[charHandle];
const QLowEnergyServicePrivate::DescData &dscrDetails = charDetails.descriptorList[descriptorHandle];
if (data.systemErrorCode != NO_ERROR) {
qCWarning(QT_BT_WINDOWS) << "Unable to get value for descriptor"
<< dscrDetails.uuid.toString()
<< "for characteristic"
<< charDetails.uuid.toString()
<< "of the service" << service->uuid.toString()
<< ":" << qt_error_string(data.systemErrorCode);
service->setError(QLowEnergyService::DescriptorReadError);
return;
}
updateValueOfDescriptor(charHandle, descriptorHandle, data.value, false);
QLowEnergyDescriptor dscr(service, charHandle, descriptorHandle);
emit service->descriptorRead(dscr, data.value);
}
break;
}
QMetaObject::invokeMethod(threadWorker, "runPendingJob", Qt::QueuedConnection);
}
void QLowEnergyControllerPrivateWin32::readDescriptor(
const QSharedPointer<QLowEnergyServicePrivate> service,
const QLowEnergyHandle charHandle,
const QLowEnergyHandle descriptorHandle)
{
Q_ASSERT(!service.isNull());
if (!service->characteristicList.contains(charHandle))
return;
const QLowEnergyServicePrivate::CharData &charDetails
= service->characteristicList[charHandle];
if (!charDetails.descriptorList.contains(descriptorHandle))
return;
ReadDescData data;
data.systemErrorCode = NO_ERROR;
data.hService = service->hService;
if (data.systemErrorCode != NO_ERROR) {
qCWarning(QT_BT_WINDOWS) << "Unable to open service" << service->uuid.toString()
<< ":" << qt_error_string(data.systemErrorCode);
service->setError(QLowEnergyService::DescriptorReadError);
return;
}
const QLowEnergyServicePrivate::DescData &dscrDetails
= charDetails.descriptorList[descriptorHandle];
data.gattDescriptor = recoverNativeLeGattDescriptor(
service->startHandle, charHandle, descriptorHandle, dscrDetails);
ThreadWorkerJob job;
job.operation = ThreadWorkerJob::ReadDescr;
job.data = QVariant::fromValue(data);
QMetaObject::invokeMethod(threadWorker, "putJob", Qt::QueuedConnection,
Q_ARG(ThreadWorkerJob, job));
}
void QLowEnergyControllerPrivateWin32::writeDescriptor(
const QSharedPointer<QLowEnergyServicePrivate> service,
const QLowEnergyHandle charHandle,
const QLowEnergyHandle descriptorHandle,
const QByteArray &newValue)
{
Q_ASSERT(!service.isNull());
if (!service->characteristicList.contains(charHandle))
return;
QLowEnergyServicePrivate::CharData &charDetails
= service->characteristicList[charHandle];
if (!charDetails.descriptorList.contains(descriptorHandle))
return;
WriteDescData data;
data.systemErrorCode = NO_ERROR;
data.newValue = newValue;
data.hService = service->hService;
if (data.systemErrorCode != NO_ERROR) {
qCWarning(QT_BT_WINDOWS) << "Unable to open service" << service->uuid.toString()
<< ":" << qt_error_string(data.systemErrorCode);
service->setError(QLowEnergyService::DescriptorWriteError);
return;
}
const QLowEnergyServicePrivate::DescData &dscrDetails
= charDetails.descriptorList[descriptorHandle];
data.gattDescriptor = recoverNativeLeGattDescriptor(
service->startHandle, charHandle, descriptorHandle, dscrDetails);
ThreadWorkerJob job;
job.operation = ThreadWorkerJob::WriteDescr;
job.data = QVariant::fromValue(data);
QMetaObject::invokeMethod(threadWorker, "putJob", Qt::QueuedConnection,
Q_ARG(ThreadWorkerJob, job));
}
void QLowEnergyControllerPrivateWin32::addToGenericAttributeList(const QLowEnergyServiceData &, QLowEnergyHandle)
{
Q_UNIMPLEMENTED();
}
void ThreadWorker::putJob(const ThreadWorkerJob &job)
{
m_jobs.append(job);
if (m_jobs.count() == 1)
runPendingJob();
}
void ThreadWorker::runPendingJob()
{
if (!m_jobs.count())
return;
ThreadWorkerJob job = m_jobs.first();
switch (job.operation) {
case ThreadWorkerJob::WriteChar:
{
WriteCharData data = job.data.value<WriteCharData>();
setGattCharacteristicValue(data.hService, &data.gattCharacteristic,
data.newValue, data.flags, &data.systemErrorCode);
job.data = QVariant::fromValue(data);
}
break;
case ThreadWorkerJob::ReadChar:
{
ReadCharData data = job.data.value<ReadCharData>();
data.value = getGattCharacteristicValue(
data.hService, &data.gattCharacteristic, &data.systemErrorCode);
job.data = QVariant::fromValue(data);
}
break;
case ThreadWorkerJob::WriteDescr:
{
WriteDescData data = job.data.value<WriteDescData>();
setGattDescriptorValue(data.hService, &data.gattDescriptor,
data.newValue, &data.systemErrorCode);
job.data = QVariant::fromValue(data);
}
break;
case ThreadWorkerJob::ReadDescr:
{
ReadDescData data = job.data.value<ReadDescData>();
data.value = getGattDescriptorValue(
data.hService,
const_cast<PBTH_LE_GATT_DESCRIPTOR>(&data.gattDescriptor),
&data.systemErrorCode);
job.data = QVariant::fromValue(data);
}
break;
}
m_jobs.removeFirst();
emit jobFinished(job);
}
QT_END_NAMESPACE