/**************************************************************************** ** ** Copyright (C) 2016 The Qt Company Ltd. ** 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_p.h" #include "qbluetoothdevicediscoveryagent.h" #include "osx/osxbtledeviceinquiry_p.h" #ifdef Q_OS_MACOS #include "osx/osxbtdeviceinquiry_p.h" #include "osx/osxbtsdpinquiry_p.h" #endif // Q_OS_MACOS #include "qbluetoothdeviceinfo.h" #include "osx/osxbtnotifier_p.h" #include "osx/osxbtutility_p.h" #include "osx/osxbluetooth_p.h" #include "osx/uistrings_p.h" #include "qbluetoothhostinfo.h" #include "qbluetoothaddress.h" #include "osx/uistrings_p.h" #include "qbluetoothuuid.h" #include "osx/btraii_p.h" #include #include #include #include #include #include #include QT_BEGIN_NAMESPACE namespace { void registerQDeviceDiscoveryMetaType() { static bool initDone = false; if (!initDone) { qRegisterMetaType(); qRegisterMetaType(); initDone = true; } } #ifdef Q_OS_MACOS using InquiryObjC = QT_MANGLE_NAMESPACE(OSXBTDeviceInquiry); #endif // Q_OS_MACOS using LEInquiryObjC = QT_MANGLE_NAMESPACE(OSXBTLEDeviceInquiry); } //namespace QBluetoothDeviceDiscoveryAgentPrivate::QBluetoothDeviceDiscoveryAgentPrivate(const QBluetoothAddress &adapter, QBluetoothDeviceDiscoveryAgent *q) : inquiryType(QBluetoothDeviceDiscoveryAgent::GeneralUnlimitedInquiry), lastError(QBluetoothDeviceDiscoveryAgent::NoError), agentState(NonActive), adapterAddress(adapter), startPending(false), stopPending(false), lowEnergySearchTimeout(OSXBluetooth::defaultLEScanTimeoutMS), #ifdef Q_OS_MACOS requestedMethods(QBluetoothDeviceDiscoveryAgent::ClassicMethod | QBluetoothDeviceDiscoveryAgent::LowEnergyMethod), #else requestedMethods(QBluetoothDeviceDiscoveryAgent::ClassicMethod), #endif // Q_OS_MACOS q_ptr(q) { registerQDeviceDiscoveryMetaType(); Q_ASSERT_X(q != nullptr, Q_FUNC_INFO, "invalid q_ptr (null)"); #ifdef Q_OS_MACOS IOBluetoothHostController *hostController = [IOBluetoothHostController defaultController]; if (!hostController || [hostController powerState] != kBluetoothHCIPowerStateON) { qCCritical(QT_BT_OSX) << "no default host controller or adapter is off"; return; } controller.reset(hostController, DarwinBluetooth::RetainPolicy::doInitialRetain); #endif } QBluetoothDeviceDiscoveryAgentPrivate::~QBluetoothDeviceDiscoveryAgentPrivate() { if (inquiryLE && agentState != NonActive) { // We want the LE scan to stop as soon as possible. if (dispatch_queue_t leQueue = OSXBluetooth::qt_LE_queue()) { // Local variable to be retained ... LEInquiryObjC *inq = inquiryLE.getAs(); dispatch_sync(leQueue, ^{ [inq stop]; }); } } } bool QBluetoothDeviceDiscoveryAgentPrivate::isActive() const { if (startPending) return true; if (stopPending) return false; return agentState != NonActive; } void QBluetoothDeviceDiscoveryAgentPrivate::start(QBluetoothDeviceDiscoveryAgent::DiscoveryMethods methods) { Q_ASSERT(!isActive()); Q_ASSERT(lastError != QBluetoothDeviceDiscoveryAgent::InvalidBluetoothAdapterError); Q_ASSERT(methods & (QBluetoothDeviceDiscoveryAgent::ClassicMethod | QBluetoothDeviceDiscoveryAgent::LowEnergyMethod)); #ifdef Q_OS_MACOS if (!controller) { setError(QBluetoothDeviceDiscoveryAgent::InvalidBluetoothAdapterError); emit q_ptr->error(lastError); return; } #endif // Q_OS_MACOS requestedMethods = methods; if (stopPending) { startPending = true; return; } // This function (re)starts the scan(s) from the scratch; // starting from Classic if it's in 'methods' (or LE scan if not). agentState = NonActive; discoveredDevices.clear(); setError(QBluetoothDeviceDiscoveryAgent::NoError); #ifdef Q_OS_MACOS if (requestedMethods & QBluetoothDeviceDiscoveryAgent::ClassicMethod) return startClassic(); #endif // Q_OS_MACOS startLE(); } #ifdef Q_OS_MACOS void QBluetoothDeviceDiscoveryAgentPrivate::startClassic() { Q_ASSERT(!isActive()); Q_ASSERT(lastError == QBluetoothDeviceDiscoveryAgent::NoError); Q_ASSERT(requestedMethods & QBluetoothDeviceDiscoveryAgent::ClassicMethod); Q_ASSERT(agentState == NonActive); OSXBluetooth::qt_test_iobluetooth_runloop(); if (!inquiry) { // The first Classic scan for this DDA. inquiry.reset([[InquiryObjC alloc] initWithDelegate:this], DarwinBluetooth::RetainPolicy::noInitialRetain); if (!inquiry) { qCCritical(QT_BT_OSX) << "failed to initialize an Classic device inquiry"; setError(QBluetoothDeviceDiscoveryAgent::UnknownError, QCoreApplication::translate(DEV_DISCOVERY, DD_NOT_STARTED)); emit q_ptr->error(lastError); return; } } agentState = ClassicScan; const IOReturn res = [inquiry.getAs() start]; if (res != kIOReturnSuccess) { setError(res, QCoreApplication::translate(DEV_DISCOVERY, DD_NOT_STARTED)); agentState = NonActive; emit q_ptr->error(lastError); } } #endif // Q_OS_MACOS void QBluetoothDeviceDiscoveryAgentPrivate::startLE() { Q_ASSERT(lastError != QBluetoothDeviceDiscoveryAgent::InvalidBluetoothAdapterError); Q_ASSERT(requestedMethods & QBluetoothDeviceDiscoveryAgent::LowEnergyMethod); using namespace OSXBluetooth; QScopedPointer notifier(new LECBManagerNotifier); // Connections: using ErrMemFunPtr = void (LECBManagerNotifier::*)(QBluetoothDeviceDiscoveryAgent::Error); notifier->connect(notifier.data(), ErrMemFunPtr(&LECBManagerNotifier::CBManagerError), this, &QBluetoothDeviceDiscoveryAgentPrivate::LEinquiryError); notifier->connect(notifier.data(), &LECBManagerNotifier::LEnotSupported, this, &QBluetoothDeviceDiscoveryAgentPrivate::LEnotSupported); notifier->connect(notifier.data(), &LECBManagerNotifier::discoveryFinished, this, &QBluetoothDeviceDiscoveryAgentPrivate::LEinquiryFinished); using DeviceMemFunPtr = void (QBluetoothDeviceDiscoveryAgentPrivate::*)(const QBluetoothDeviceInfo &); notifier->connect(notifier.data(), &LECBManagerNotifier::deviceDiscovered, this, DeviceMemFunPtr(&QBluetoothDeviceDiscoveryAgentPrivate::deviceFound)); // Check queue and create scanner: inquiryLE.reset([[LEInquiryObjC alloc] initWithNotifier:notifier.data()], DarwinBluetooth::RetainPolicy::noInitialRetain); if (inquiryLE) notifier.take(); // Whatever happens next, inquiryLE is already the owner ... dispatch_queue_t leQueue(qt_LE_queue()); if (!leQueue || !inquiryLE) { setError(QBluetoothDeviceDiscoveryAgent::UnknownError, QCoreApplication::translate(DEV_DISCOVERY, DD_NOT_STARTED_LE)); agentState = NonActive; emit q_ptr->error(lastError); return; } // Now start in on LE queue: agentState = LEScan; // We need the local variable so that it's retained ... LEInquiryObjC *inq = inquiryLE.getAs(); dispatch_async(leQueue, ^{ [inq startWithTimeout:lowEnergySearchTimeout]; }); } void QBluetoothDeviceDiscoveryAgentPrivate::stop() { Q_ASSERT_X(isActive(), Q_FUNC_INFO, "called whithout active inquiry"); Q_ASSERT_X(lastError != QBluetoothDeviceDiscoveryAgent::InvalidBluetoothAdapterError, Q_FUNC_INFO, "called with invalid bluetooth adapter"); using namespace OSXBluetooth; const bool prevStart = startPending; startPending = false; stopPending = true; setError(QBluetoothDeviceDiscoveryAgent::NoError); #ifdef Q_OS_MACOS if (agentState == ClassicScan) { const IOReturn res = [inquiry.getAs() stop]; if (res != kIOReturnSuccess) { qCWarning(QT_BT_OSX) << "failed to stop"; startPending = prevStart; stopPending = false; setError(res, QCoreApplication::translate(DEV_DISCOVERY, DD_NOT_STOPPED)); emit q_ptr->error(lastError); } } else { #else { Q_UNUSED(prevStart) #endif // Q_OS_MACOS dispatch_queue_t leQueue(qt_LE_queue()); Q_ASSERT(leQueue); // We need the local variable so that it's retained ... LEInquiryObjC *inq = inquiryLE.getAs(); dispatch_sync(leQueue, ^{ [inq stop]; }); // We consider LE scan to be stopped immediately and // do not care about this LEDeviceInquiry object anymore. LEinquiryFinished(); } } #ifdef Q_OS_MACOS void QBluetoothDeviceDiscoveryAgentPrivate::inquiryFinished() { // The subsequent start(LE) function (if any) // will (re)set the correct state. agentState = NonActive; if (stopPending && !startPending) { stopPending = false; emit q_ptr->canceled(); } else if (startPending) { startPending = false; stopPending = false; start(requestedMethods); } else { // We can be here _only_ if a classic scan // finished in a normal way (not cancelled) // and requestedMethods includes LowEnergyMethod. // startLE() will take care of old devices // not supporting Bluetooth 4.0. if (requestedMethods & QBluetoothDeviceDiscoveryAgent::LowEnergyMethod) startLE(); else emit q_ptr->finished(); } } void QBluetoothDeviceDiscoveryAgentPrivate::error(IOReturn error) { startPending = false; stopPending = false; setError(error); emit q_ptr->error(lastError); } void QBluetoothDeviceDiscoveryAgentPrivate::classicDeviceFound(void *obj) { auto device = static_cast(obj); Q_ASSERT_X(device, Q_FUNC_INFO, "invalid IOBluetoothDevice (nil)"); Q_ASSERT_X(agentState == ClassicScan, Q_FUNC_INFO, "invalid agent state (expected classic scan)"); QT_BT_MAC_AUTORELEASEPOOL; // Let's collect some info about this device: const QBluetoothAddress deviceAddress(OSXBluetooth::qt_address([device getAddress])); if (deviceAddress.isNull()) { qCWarning(QT_BT_OSX) << "invalid Bluetooth address"; return; } QString deviceName; if (device.name) deviceName = QString::fromNSString(device.name); const auto classOfDevice = qint32(device.classOfDevice); QBluetoothDeviceInfo deviceInfo(deviceAddress, deviceName, classOfDevice); deviceInfo.setCoreConfigurations(QBluetoothDeviceInfo::BaseRateCoreConfiguration); deviceInfo.setRssi(device.RSSI); const QVector uuids(OSXBluetooth::extract_services_uuids(device)); deviceInfo.setServiceUuids(uuids); deviceFound(deviceInfo); } void QBluetoothDeviceDiscoveryAgentPrivate::setError(IOReturn error, const QString &text) { if (error == kIOReturnSuccess) setError(QBluetoothDeviceDiscoveryAgent::NoError, text); else if (error == kIOReturnNoPower) setError(QBluetoothDeviceDiscoveryAgent::PoweredOffError, text); else setError(QBluetoothDeviceDiscoveryAgent::UnknownError, text); } #endif // Q_OS_MACOS void QBluetoothDeviceDiscoveryAgentPrivate::setError(QBluetoothDeviceDiscoveryAgent::Error error, const QString &text) { lastError = error; if (text.length() > 0) { errorString = text; } else { switch (lastError) { case QBluetoothDeviceDiscoveryAgent::NoError: errorString = QString(); break; case QBluetoothDeviceDiscoveryAgent::PoweredOffError: errorString = QCoreApplication::translate(DEV_DISCOVERY, DD_POWERED_OFF); break; case QBluetoothDeviceDiscoveryAgent::InvalidBluetoothAdapterError: errorString = QCoreApplication::translate(DEV_DISCOVERY, DD_INVALID_ADAPTER); break; case QBluetoothDeviceDiscoveryAgent::InputOutputError: errorString = QCoreApplication::translate(DEV_DISCOVERY, DD_IO); break; case QBluetoothDeviceDiscoveryAgent::UnsupportedPlatformError: errorString = QCoreApplication::translate(DEV_DISCOVERY, DD_NOTSUPPORTED); break; case QBluetoothDeviceDiscoveryAgent::UnknownError: default: errorString = QCoreApplication::translate(DEV_DISCOVERY, DD_UNKNOWN_ERROR); } } } void QBluetoothDeviceDiscoveryAgentPrivate::LEinquiryError(QBluetoothDeviceDiscoveryAgent::Error error) { Q_ASSERT(error == QBluetoothDeviceDiscoveryAgent::PoweredOffError || error == QBluetoothDeviceDiscoveryAgent::UnsupportedDiscoveryMethod); inquiryLE.reset(); startPending = false; stopPending = false; agentState = NonActive; setError(error); emit q_ptr->error(lastError); } void QBluetoothDeviceDiscoveryAgentPrivate::LEnotSupported() { qCDebug(QT_BT_OSX) << "no Bluetooth LE support"; #ifdef Q_OS_MACOS if (requestedMethods & QBluetoothDeviceDiscoveryAgent::ClassicMethod) { // Having both Classic | LE means this is not an error. LEinquiryFinished(); } else { // In the past this was never an error, that's why we have // LEnotSupported as a special method. But now, since // we can have separate Classic/LE scans, we have to report it // as UnsupportedDiscoveryMethod. LEinquiryError(QBluetoothDeviceDiscoveryAgent::UnsupportedDiscoveryMethod); } #else inquiryLE.reset(); startPending = false; stopPending = false; setError(QBluetoothDeviceDiscoveryAgent::UnsupportedPlatformError); emit q_ptr->error(lastError); #endif } void QBluetoothDeviceDiscoveryAgentPrivate::LEinquiryFinished() { // The same logic as in inquiryFinished, but does not start LE scan. agentState = NonActive; inquiryLE.reset(); if (stopPending && !startPending) { stopPending = false; emit q_ptr->canceled(); } else if (startPending) { startPending = false; stopPending = false; start(requestedMethods); //Start again. } else { emit q_ptr->finished(); } } void QBluetoothDeviceDiscoveryAgentPrivate::deviceFound(const QBluetoothDeviceInfo &newDeviceInfo) { // Core Bluetooth does not allow us to access addresses, we have to use uuid instead. // This uuid has nothing to do with uuids in Bluetooth in general (it's generated by // Apple's framework using some algorithm), but it's a 128-bit uuid after all. const bool isLE = #ifdef Q_OS_MACOS newDeviceInfo.coreConfigurations() == QBluetoothDeviceInfo::LowEnergyCoreConfiguration; #else true; #endif // Q_OS_MACOS for (int i = 0, e = discoveredDevices.size(); i < e; ++i) { if (isLE) { if (discoveredDevices[i].deviceUuid() == newDeviceInfo.deviceUuid()) { QBluetoothDeviceInfo::Fields updatedFields = QBluetoothDeviceInfo::Field::None; if (discoveredDevices[i].rssi() != newDeviceInfo.rssi()) { qCDebug(QT_BT_OSX) << "Updating RSSI for" << newDeviceInfo.address() << newDeviceInfo.rssi(); discoveredDevices[i].setRssi(newDeviceInfo.rssi()); updatedFields.setFlag(QBluetoothDeviceInfo::Field::RSSI); } if (discoveredDevices[i].manufacturerData() != newDeviceInfo.manufacturerData()) { qCDebug(QT_BT_OSX) << "Updating manufacturer data for" << newDeviceInfo.address(); const QVector keys = newDeviceInfo.manufacturerIds(); for (auto key: keys) discoveredDevices[i].setManufacturerData(key, newDeviceInfo.manufacturerData(key)); updatedFields.setFlag(QBluetoothDeviceInfo::Field::ManufacturerData); } if (lowEnergySearchTimeout > 0) { if (discoveredDevices[i] != newDeviceInfo) { discoveredDevices.replace(i, newDeviceInfo); emit q_ptr->deviceDiscovered(newDeviceInfo); } else { if (!updatedFields.testFlag(QBluetoothDeviceInfo::Field::None)) emit q_ptr->deviceUpdated(discoveredDevices[i], updatedFields); } return; } discoveredDevices.replace(i, newDeviceInfo); emit q_ptr->deviceDiscovered(newDeviceInfo); if (!updatedFields.testFlag(QBluetoothDeviceInfo::Field::None)) emit q_ptr->deviceUpdated(discoveredDevices[i], updatedFields); return; } } else { #ifdef Q_OS_MACOS if (discoveredDevices[i].address() == newDeviceInfo.address()) { if (discoveredDevices[i] == newDeviceInfo) return; discoveredDevices.replace(i, newDeviceInfo); emit q_ptr->deviceDiscovered(newDeviceInfo); return; } #else Q_UNREACHABLE(); #endif // Q_OS_MACOS } } discoveredDevices.append(newDeviceInfo); emit q_ptr->deviceDiscovered(newDeviceInfo); } QBluetoothDeviceDiscoveryAgent::DiscoveryMethods QBluetoothDeviceDiscoveryAgent::supportedDiscoveryMethods() { #ifdef Q_OS_MACOS return ClassicMethod | LowEnergyMethod; #else return LowEnergyMethod; #endif // Q_OS_MACOS } QT_END_NAMESPACE