| /**************************************************************************** |
| ** |
| ** 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 "qbluetoothdevicediscoveryagent.h" |
| #include "qbluetoothuuid.h" |
| #include "qbluetoothdevicediscoveryagent_p.h" |
| #include "qbluetoothlocaldevice_p.h" |
| |
| #include <QtCore/qmutex.h> |
| #include <QtCore/QThread> |
| #include <QtCore/QLoggingCategory> |
| |
| #include <qt_windows.h> |
| #include <setupapi.h> |
| #include <bluetoothapis.h> |
| |
| QT_BEGIN_NAMESPACE |
| |
| Q_DECLARE_LOGGING_CATEGORY(QT_BT_WINDOWS) |
| |
| struct LeDeviceEntry { |
| QString devicePath; |
| QBluetoothAddress deviceAddress; |
| }; |
| |
| Q_GLOBAL_STATIC(QVector<LeDeviceEntry>, cachedLeDeviceEntries) |
| Q_GLOBAL_STATIC(QMutex, cachedLeDeviceEntriesGuard) |
| |
| static QString devicePropertyString( |
| HDEVINFO hDeviceInfo, |
| const PSP_DEVINFO_DATA deviceInfoData, |
| DWORD registryProperty) |
| { |
| DWORD propertyRegDataType = 0; |
| DWORD propertyBufferSize = 0; |
| QByteArray propertyBuffer; |
| |
| while (!::SetupDiGetDeviceRegistryProperty( |
| hDeviceInfo, |
| deviceInfoData, |
| registryProperty, |
| &propertyRegDataType, |
| propertyBuffer.isEmpty() ? nullptr : reinterpret_cast<PBYTE>(propertyBuffer.data()), |
| propertyBuffer.size(), |
| &propertyBufferSize)) { |
| |
| const DWORD error = ::GetLastError(); |
| if (error != ERROR_INSUFFICIENT_BUFFER |
| || (propertyRegDataType != REG_SZ |
| && propertyRegDataType != REG_EXPAND_SZ)) { |
| return QString(); |
| } |
| |
| // add +2 byte to allow to successfully convert to qstring |
| propertyBuffer.fill(0, propertyBufferSize + sizeof(wchar_t)); |
| } |
| |
| return QString::fromWCharArray(reinterpret_cast<const wchar_t *>( |
| propertyBuffer.constData())); |
| } |
| |
| static QString deviceName(HDEVINFO hDeviceInfo, PSP_DEVINFO_DATA deviceInfoData) |
| { |
| return devicePropertyString(hDeviceInfo, deviceInfoData, SPDRP_FRIENDLYNAME); |
| } |
| |
| static QString deviceSystemPath(const PSP_INTERFACE_DEVICE_DETAIL_DATA detailData) |
| { |
| return QString::fromWCharArray(detailData->DevicePath); |
| } |
| |
| static QBluetoothAddress deviceAddress(const QString &devicePath) |
| { |
| const int firstbound = devicePath.indexOf(QStringLiteral("dev_")); |
| const int lastbound = devicePath.indexOf(QLatin1Char('#'), firstbound); |
| const QString hex = devicePath.mid(firstbound + 4, lastbound - firstbound - 4); |
| bool ok = false; |
| return QBluetoothAddress(hex.toULongLong(&ok, 16)); |
| } |
| |
| static QBluetoothDeviceInfo createClassicDeviceInfo(const BLUETOOTH_DEVICE_INFO &foundDevice) |
| { |
| QBluetoothDeviceInfo deviceInfo( |
| QBluetoothAddress(foundDevice.Address.ullLong), |
| QString::fromWCharArray(foundDevice.szName), |
| foundDevice.ulClassofDevice); |
| |
| deviceInfo.setCoreConfigurations(QBluetoothDeviceInfo::BaseRateCoreConfiguration); |
| |
| if (foundDevice.fRemembered) |
| deviceInfo.setCached(true); |
| return deviceInfo; |
| } |
| |
| static QBluetoothDeviceInfo findFirstClassicDevice( |
| DWORD *systemErrorCode, HBLUETOOTH_DEVICE_FIND *hSearch) |
| { |
| BLUETOOTH_DEVICE_SEARCH_PARAMS searchParams = {}; |
| searchParams.dwSize = sizeof(searchParams); |
| searchParams.cTimeoutMultiplier = 10; // 12.8 sec |
| searchParams.fIssueInquiry = TRUE; |
| searchParams.fReturnAuthenticated = TRUE; |
| searchParams.fReturnConnected = TRUE; |
| searchParams.fReturnRemembered = TRUE; |
| searchParams.fReturnUnknown = TRUE; |
| searchParams.hRadio = nullptr; |
| |
| BLUETOOTH_DEVICE_INFO deviceInfo = {}; |
| deviceInfo.dwSize = sizeof(deviceInfo); |
| |
| const HBLUETOOTH_DEVICE_FIND hFind = ::BluetoothFindFirstDevice( |
| &searchParams, &deviceInfo); |
| |
| QBluetoothDeviceInfo foundDevice; |
| if (hFind) { |
| *hSearch = hFind; |
| *systemErrorCode = NO_ERROR; |
| foundDevice = createClassicDeviceInfo(deviceInfo); |
| } else { |
| *systemErrorCode = int(::GetLastError()); |
| } |
| |
| return foundDevice; |
| } |
| |
| static QBluetoothDeviceInfo findNextClassicDevice( |
| DWORD *systemErrorCode, HBLUETOOTH_DEVICE_FIND hSearch) |
| { |
| BLUETOOTH_DEVICE_INFO deviceInfo = {}; |
| deviceInfo.dwSize = sizeof(deviceInfo); |
| |
| QBluetoothDeviceInfo foundDevice; |
| if (!::BluetoothFindNextDevice(hSearch, &deviceInfo)) { |
| *systemErrorCode = int(::GetLastError()); |
| } else { |
| *systemErrorCode = NO_ERROR; |
| foundDevice = createClassicDeviceInfo(deviceInfo); |
| } |
| |
| return foundDevice; |
| } |
| |
| static void closeClassicSearch(HBLUETOOTH_DEVICE_FIND hSearch) |
| { |
| if (hSearch) |
| ::BluetoothFindDeviceClose(hSearch); |
| } |
| |
| static QVector<QBluetoothDeviceInfo> enumerateLeDevices( |
| DWORD *systemErrorCode) |
| { |
| // GUID_BLUETOOTHLE_DEVICE_INTERFACE |
| const QUuid deviceInterfaceGuid("781aee18-7733-4ce4-add0-91f41c67b592"); |
| const HDEVINFO hDeviceInfo = ::SetupDiGetClassDevs( |
| reinterpret_cast<const GUID *>(&deviceInterfaceGuid), |
| nullptr, |
| nullptr, |
| DIGCF_PRESENT | DIGCF_DEVICEINTERFACE); |
| |
| if (hDeviceInfo == INVALID_HANDLE_VALUE) { |
| *systemErrorCode = int(::GetLastError()); |
| return QVector<QBluetoothDeviceInfo>(); |
| } |
| |
| QVector<QBluetoothDeviceInfo> foundDevices; |
| DWORD index = 0; |
| |
| QVector<LeDeviceEntry> cachedEntries; |
| |
| for (;;) { |
| SP_DEVICE_INTERFACE_DATA deviceInterfaceData = {}; |
| deviceInterfaceData.cbSize = sizeof(deviceInterfaceData); |
| |
| if (!::SetupDiEnumDeviceInterfaces( |
| hDeviceInfo, |
| nullptr, |
| reinterpret_cast<const GUID *>(&deviceInterfaceGuid), |
| index++, |
| &deviceInterfaceData)) { |
| *systemErrorCode = int(::GetLastError()); |
| break; |
| } |
| |
| DWORD deviceInterfaceDetailDataSize = 0; |
| if (!::SetupDiGetDeviceInterfaceDetail( |
| hDeviceInfo, |
| &deviceInterfaceData, |
| nullptr, |
| deviceInterfaceDetailDataSize, |
| &deviceInterfaceDetailDataSize, |
| nullptr)) { |
| if (::GetLastError() != ERROR_INSUFFICIENT_BUFFER) { |
| *systemErrorCode = int(::GetLastError()); |
| break; |
| } |
| } |
| |
| SP_DEVINFO_DATA deviceInfoData = {}; |
| deviceInfoData.cbSize = sizeof(deviceInfoData); |
| |
| QByteArray deviceInterfaceDetailDataBuffer( |
| 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( |
| hDeviceInfo, |
| &deviceInterfaceData, |
| deviceInterfaceDetailData, |
| deviceInterfaceDetailDataBuffer.size(), |
| &deviceInterfaceDetailDataSize, |
| &deviceInfoData)) { |
| *systemErrorCode = int(::GetLastError()); |
| break; |
| } |
| |
| const QString systemPath = deviceSystemPath(deviceInterfaceDetailData); |
| const QBluetoothAddress address = deviceAddress(systemPath); |
| if (address.isNull()) |
| continue; |
| const QString name = deviceName(hDeviceInfo, &deviceInfoData); |
| |
| QBluetoothDeviceInfo deviceInfo(address, name, QBluetoothDeviceInfo::MiscellaneousDevice); |
| deviceInfo.setCoreConfigurations(QBluetoothDeviceInfo::LowEnergyCoreConfiguration); |
| deviceInfo.setCached(true); |
| |
| foundDevices << deviceInfo; |
| cachedEntries << LeDeviceEntry{systemPath, address}; |
| } |
| |
| QMutexLocker locker(cachedLeDeviceEntriesGuard()); |
| cachedLeDeviceEntries()->swap(cachedEntries); |
| |
| ::SetupDiDestroyDeviceInfoList(hDeviceInfo); |
| return foundDevices; |
| } |
| |
| struct DiscoveryResult { |
| QVector<QBluetoothDeviceInfo> devices; |
| DWORD systemErrorCode; |
| HBLUETOOTH_DEVICE_FIND hSearch; // Used only for classic devices |
| }; |
| |
| QString QBluetoothDeviceDiscoveryAgentPrivate::discoveredLeDeviceSystemPath( |
| const QBluetoothAddress &deviceAddress) |
| { |
| // update LE devices cache |
| DWORD dummyErrorCode; |
| enumerateLeDevices(&dummyErrorCode); |
| |
| QMutexLocker locker(cachedLeDeviceEntriesGuard()); |
| for (const LeDeviceEntry &e: *cachedLeDeviceEntries) { |
| if (e.deviceAddress == deviceAddress) |
| return e.devicePath; |
| } |
| return QString(); |
| } |
| |
| QBluetoothDeviceDiscoveryAgentPrivate::QBluetoothDeviceDiscoveryAgentPrivate( |
| const QBluetoothAddress &deviceAdapter, |
| QBluetoothDeviceDiscoveryAgent *parent) |
| : inquiryType(QBluetoothDeviceDiscoveryAgent::GeneralUnlimitedInquiry) |
| , lastError(QBluetoothDeviceDiscoveryAgent::NoError) |
| , adapterAddress(deviceAdapter) |
| , pendingCancel(false) |
| , pendingStart(false) |
| , active(false) |
| , lowEnergySearchTimeout(-1) // remains -1 -> timeout not supported |
| , q_ptr(parent) |
| { |
| threadLE = new QThread; |
| threadWorkerLE = new ThreadWorkerDeviceDiscovery; |
| threadWorkerLE->moveToThread(threadLE); |
| connect(threadWorkerLE, &ThreadWorkerDeviceDiscovery::discoveryCompleted, this, &QBluetoothDeviceDiscoveryAgentPrivate::completeLeDevicesDiscovery); |
| connect(threadLE, &QThread::finished, threadWorkerLE, &ThreadWorkerDeviceDiscovery::deleteLater); |
| connect(threadLE, &QThread::finished, threadLE, &QThread::deleteLater); |
| threadLE->start(); |
| |
| threadClassic = new QThread; |
| threadWorkerClassic = new ThreadWorkerDeviceDiscovery; |
| threadWorkerClassic->moveToThread(threadClassic); |
| connect(threadWorkerClassic, &ThreadWorkerDeviceDiscovery::discoveryCompleted, this, &QBluetoothDeviceDiscoveryAgentPrivate::completeClassicDevicesDiscovery); |
| connect(threadClassic, &QThread::finished, threadWorkerClassic, &ThreadWorkerDeviceDiscovery::deleteLater); |
| connect(threadClassic, &QThread::finished, threadClassic, &QThread::deleteLater); |
| threadClassic->start(); |
| } |
| |
| QBluetoothDeviceDiscoveryAgentPrivate::~QBluetoothDeviceDiscoveryAgentPrivate() |
| { |
| if (active) |
| stop(); |
| if (threadLE) |
| threadLE->quit(); |
| if (threadClassic) |
| threadClassic->quit(); |
| } |
| |
| bool QBluetoothDeviceDiscoveryAgentPrivate::isActive() const |
| { |
| if (pendingStart) |
| return true; |
| if (pendingCancel) |
| return false; |
| return active; |
| } |
| |
| QBluetoothDeviceDiscoveryAgent::DiscoveryMethods QBluetoothDeviceDiscoveryAgent::supportedDiscoveryMethods() |
| { |
| return (LowEnergyMethod | ClassicMethod); |
| } |
| |
| void QBluetoothDeviceDiscoveryAgentPrivate::start(QBluetoothDeviceDiscoveryAgent::DiscoveryMethods methods) |
| { |
| requestedMethods = methods; |
| |
| if (pendingCancel == true) { |
| pendingStart = true; |
| return; |
| } |
| |
| const QList<QBluetoothHostInfo> foundLocalAdapters = |
| QBluetoothLocalDevicePrivate::localAdapters(); |
| |
| Q_Q(QBluetoothDeviceDiscoveryAgent); |
| |
| if (foundLocalAdapters.isEmpty()) { |
| qCWarning(QT_BT_WINDOWS) << "Device does not support Bluetooth"; |
| lastError = QBluetoothDeviceDiscoveryAgent::InputOutputError; |
| errorString = QBluetoothDeviceDiscoveryAgent::tr("Device does not support Bluetooth"); |
| emit q->error(lastError); |
| return; |
| } |
| |
| // Check for the local adapter address. |
| auto equals = [this](const QBluetoothHostInfo &adapterInfo) { |
| return adapterAddress == QBluetoothAddress() |
| || adapterAddress == adapterInfo.address(); |
| }; |
| const auto end = foundLocalAdapters.cend(); |
| const auto it = std::find_if(foundLocalAdapters.cbegin(), end, equals); |
| if (it == end) { |
| qCWarning(QT_BT_WINDOWS) << "Incorrect local adapter passed."; |
| lastError = QBluetoothDeviceDiscoveryAgent::InvalidBluetoothAdapterError; |
| errorString = QBluetoothDeviceDiscoveryAgent::tr("Passed address is not a local device."); |
| emit q->error(lastError); |
| return; |
| } |
| |
| discoveredDevices.clear(); |
| active = true; |
| |
| // We run LE search first, as it is fast on windows. |
| if (requestedMethods & QBluetoothDeviceDiscoveryAgent::LowEnergyMethod) |
| startLeDevicesDiscovery(); |
| else if (requestedMethods & QBluetoothDeviceDiscoveryAgent::ClassicMethod) |
| startClassicDevicesDiscovery(); |
| } |
| |
| void QBluetoothDeviceDiscoveryAgentPrivate::stop() |
| { |
| if (!active) |
| return; |
| |
| pendingCancel = true; |
| pendingStart = false; |
| } |
| |
| void QBluetoothDeviceDiscoveryAgentPrivate::cancelDiscovery() |
| { |
| Q_Q(QBluetoothDeviceDiscoveryAgent); |
| active = false; |
| pendingCancel = false; |
| emit q->canceled(); |
| } |
| |
| void QBluetoothDeviceDiscoveryAgentPrivate::restartDiscovery() |
| { |
| pendingStart = false; |
| pendingCancel = false; |
| start(requestedMethods); |
| } |
| |
| void QBluetoothDeviceDiscoveryAgentPrivate::finishDiscovery(QBluetoothDeviceDiscoveryAgent::Error errorCode, const QString &errorText) |
| { |
| active = false; |
| pendingStart = false; |
| pendingCancel = false; |
| lastError = errorCode; |
| errorString = errorText; |
| |
| Q_Q(QBluetoothDeviceDiscoveryAgent); |
| if (errorCode == QBluetoothDeviceDiscoveryAgent::NoError) |
| emit q->finished(); |
| else |
| emit q->error(lastError); |
| } |
| |
| void QBluetoothDeviceDiscoveryAgentPrivate::startLeDevicesDiscovery() |
| { |
| const auto threadWorker = threadWorkerLE; |
| QMetaObject::invokeMethod(threadWorkerLE, [threadWorker]() |
| { |
| DiscoveryResult result; // Do not use hSearch here! |
| result.systemErrorCode = NO_ERROR; |
| result.devices = enumerateLeDevices(&result.systemErrorCode); |
| emit threadWorker->discoveryCompleted(QVariant::fromValue(result)); |
| }, Qt::QueuedConnection); |
| } |
| |
| void QBluetoothDeviceDiscoveryAgentPrivate::completeLeDevicesDiscovery(const QVariant &res) |
| { |
| if (pendingCancel && !pendingStart) { |
| cancelDiscovery(); |
| } else if (pendingStart) { |
| restartDiscovery(); |
| } else { |
| const DiscoveryResult result = res.value<DiscoveryResult>(); |
| if (result.systemErrorCode == NO_ERROR || result.systemErrorCode == ERROR_NO_MORE_ITEMS) { |
| for (const QBluetoothDeviceInfo &device : result.devices) |
| processDiscoveredDevice(device); |
| |
| // We run classic search at second, as it is slow on windows. |
| if (requestedMethods & QBluetoothDeviceDiscoveryAgent::ClassicMethod) |
| startClassicDevicesDiscovery(); |
| else |
| finishDiscovery(QBluetoothDeviceDiscoveryAgent::NoError, qt_error_string(NO_ERROR)); |
| } else { |
| const QBluetoothDeviceDiscoveryAgent::Error error = (result.systemErrorCode == ERROR_INVALID_HANDLE) |
| ? QBluetoothDeviceDiscoveryAgent::InvalidBluetoothAdapterError |
| : QBluetoothDeviceDiscoveryAgent::InputOutputError; |
| finishDiscovery(error, qt_error_string(result.systemErrorCode)); |
| } |
| } |
| } |
| |
| void QBluetoothDeviceDiscoveryAgentPrivate::startClassicDevicesDiscovery(Qt::HANDLE hSearch) |
| { |
| const auto threadWorker = threadWorkerClassic; |
| QMetaObject::invokeMethod(threadWorker, [threadWorker, hSearch]() |
| { |
| DiscoveryResult result; |
| result.hSearch = hSearch; |
| result.systemErrorCode = NO_ERROR; |
| |
| const QBluetoothDeviceInfo device = hSearch |
| ? findNextClassicDevice(&result.systemErrorCode, result.hSearch) |
| : findFirstClassicDevice(&result.systemErrorCode, &result.hSearch); |
| |
| result.devices.append(device); |
| emit threadWorker->discoveryCompleted(QVariant::fromValue(result)); |
| }, Qt::QueuedConnection); |
| } |
| |
| void QBluetoothDeviceDiscoveryAgentPrivate::completeClassicDevicesDiscovery(const QVariant &res) |
| { |
| const DiscoveryResult result = res.value<DiscoveryResult>(); |
| if (pendingCancel && !pendingStart) { |
| closeClassicSearch(result.hSearch); |
| cancelDiscovery(); |
| } else if (pendingStart) { |
| closeClassicSearch(result.hSearch); |
| restartDiscovery(); |
| } else { |
| if (result.systemErrorCode == ERROR_NO_MORE_ITEMS) { |
| closeClassicSearch(result.hSearch); |
| finishDiscovery(QBluetoothDeviceDiscoveryAgent::NoError, QString()); |
| } else if (result.systemErrorCode == NO_ERROR) { |
| if (result.hSearch) { |
| for (const QBluetoothDeviceInfo &device : result.devices) |
| processDiscoveredDevice(device); |
| |
| startClassicDevicesDiscovery(result.hSearch); |
| } else { |
| finishDiscovery(QBluetoothDeviceDiscoveryAgent::NoError, QString()); |
| } |
| } else { |
| closeClassicSearch(result.hSearch); |
| const QBluetoothDeviceDiscoveryAgent::Error error = (result.systemErrorCode == ERROR_INVALID_HANDLE) |
| ? QBluetoothDeviceDiscoveryAgent::InvalidBluetoothAdapterError |
| : QBluetoothDeviceDiscoveryAgent::InputOutputError; |
| finishDiscovery(error, qt_error_string(result.systemErrorCode)); |
| } |
| } |
| } |
| |
| void QBluetoothDeviceDiscoveryAgentPrivate::processDiscoveredDevice( |
| const QBluetoothDeviceInfo &foundDevice) |
| { |
| Q_Q(QBluetoothDeviceDiscoveryAgent); |
| |
| auto equalAddress = [foundDevice](const QBluetoothDeviceInfo &targetDevice) { |
| return foundDevice.address() == targetDevice.address(); }; |
| auto end = discoveredDevices.end(); |
| auto deviceIt = std::find_if(discoveredDevices.begin(), end, equalAddress); |
| if (deviceIt == end) { |
| qCDebug(QT_BT_WINDOWS) << "Found device: " << foundDevice.name() << foundDevice.address(); |
| discoveredDevices.append(foundDevice); |
| emit q->deviceDiscovered(foundDevice); |
| } else { |
| qCDebug(QT_BT_WINDOWS) << "Updating device:" << deviceIt->name() << deviceIt->address(); |
| // merge service uuids |
| QList<QBluetoothUuid> uuids = deviceIt->serviceUuids(); |
| uuids.append(foundDevice.serviceUuids()); |
| const QSet<QBluetoothUuid> uuidSet = uuids.toSet(); |
| if (deviceIt->serviceUuids().count() != uuidSet.count()) |
| deviceIt->setServiceUuids(uuidSet.toList().toVector()); |
| if (deviceIt->coreConfigurations() != foundDevice.coreConfigurations()) |
| deviceIt->setCoreConfigurations( |
| QBluetoothDeviceInfo::BaseRateAndLowEnergyCoreConfiguration); |
| } |
| } |
| |
| QT_END_NAMESPACE |
| |
| Q_DECLARE_METATYPE(DiscoveryResult) |