diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/bluetooth/bluetooth.pro | 3 | ||||
-rw-r--r-- | src/bluetooth/osx/osxbtledeviceinquiry.mm | 325 | ||||
-rw-r--r-- | src/bluetooth/osx/osxbtledeviceinquiry_p.h | 73 | ||||
-rw-r--r-- | src/bluetooth/osx/osxbtutility.mm | 41 | ||||
-rw-r--r-- | src/bluetooth/osx/osxbtutility_p.h | 4 | ||||
-rw-r--r-- | src/bluetooth/qbluetoothdevicediscoveryagent_ios.mm | 231 | ||||
-rw-r--r-- | src/bluetooth/qbluetoothdevicediscoveryagent_osx.mm | 168 |
7 files changed, 452 insertions, 393 deletions
diff --git a/src/bluetooth/bluetooth.pro b/src/bluetooth/bluetooth.pro index e37ad7aa..6cf0795c 100644 --- a/src/bluetooth/bluetooth.pro +++ b/src/bluetooth/bluetooth.pro @@ -164,7 +164,8 @@ config_bluez:qtHaveModule(dbus) { qlowenergyservice_osx.mm PRIVATE_HEADERS += \ - qlowenergycontroller_osx_p.h + qlowenergycontroller_osx_p.h \ + qbluetoothdevicediscoverytimer_osx_p.h include(osx/osxbt.pri) SOURCES += \ diff --git a/src/bluetooth/osx/osxbtledeviceinquiry.mm b/src/bluetooth/osx/osxbtledeviceinquiry.mm index 28bfd1bc..f3a95820 100644 --- a/src/bluetooth/osx/osxbtledeviceinquiry.mm +++ b/src/bluetooth/osx/osxbtledeviceinquiry.mm @@ -46,10 +46,6 @@ QT_BEGIN_NAMESPACE namespace OSXBluetooth { -LEDeviceInquiryDelegate::~LEDeviceInquiryDelegate() -{ -} - #if QT_MAC_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_9, __IPHONE_6_0) QBluetoothUuid qt_uuid(NSUUID *nsUuid) @@ -107,32 +103,19 @@ using namespace QT_NAMESPACE; #endif -@interface QT_MANGLE_NAMESPACE(OSXBTLEDeviceInquiry) (PrivateAPI) <CBCentralManagerDelegate, CBPeripheralDelegate> -// "Timeout" callback to stop a scan. +@interface QT_MANGLE_NAMESPACE(OSXBTLEDeviceInquiry) (PrivateAPI) <CBCentralManagerDelegate> - (void)stopScan; -- (void)handlePoweredOffAfterDelay; +- (void)handlePoweredOff; @end @implementation QT_MANGLE_NAMESPACE(OSXBTLEDeviceInquiry) -+ (int)inquiryLength -{ - // There is no default timeout, - // scan does not stop if not asked. - // Return in milliseconds - return 10 * 1000; -} - -- (id)initWithDelegate:(OSXBluetooth::LEDeviceInquiryDelegate *)aDelegate +- (id)init { - Q_ASSERT_X(aDelegate, Q_FUNC_INFO, "invalid delegate (null)"); - if (self = [super init]) { - delegate = aDelegate; - peripherals = [[NSMutableDictionary alloc] init]; - manager = nil; - scanPhase = noActivity; - cancelled = false; + uuids.reset([[NSMutableSet alloc] init]); + internalState = InquiryStarting; + state.store(int(internalState)); } return self; @@ -140,150 +123,137 @@ using namespace QT_NAMESPACE; - (void)dealloc { - [NSObject cancelPreviousPerformRequestsWithTarget:self]; - if (manager) { [manager setDelegate:nil]; - if (scanPhase == activeScan) + if (internalState == InquiryActive) [manager stopScan]; - [manager release]; } - [peripherals release]; [super dealloc]; } - (void)stopScan { - // Scan's timeout. - Q_ASSERT_X(delegate, Q_FUNC_INFO, "invalid delegate (null)"); - Q_ASSERT_X(manager, Q_FUNC_INFO, "invalid central (nil)"); - Q_ASSERT_X(scanPhase == activeScan, Q_FUNC_INFO, "invalid state"); - Q_ASSERT_X(!cancelled, Q_FUNC_INFO, "invalid state"); - - [manager setDelegate:nil]; - [manager stopScan]; - scanPhase = noActivity; - - delegate->LEdeviceInquiryFinished(); -} + // Scan's "timeout" - we consider LE device + // discovery finished. + using namespace OSXBluetooth; -- (void)handlePoweredOffAfterDelay -{ - // If we are here, this means: - // we received 'PoweredOff' while scanPhase == startingScan - // and no 'PoweredOn' after this. - - Q_ASSERT_X(delegate, Q_FUNC_INFO, "invalid delegate (null)"); - Q_ASSERT_X(scanPhase == startingScan, Q_FUNC_INFO, "invalid state"); - - scanPhase = noActivity; - if (cancelled) { - // Timeout happened before - // the second status update, but after 'stop'. - delegate->LEdeviceInquiryFinished(); - } else { - // Timeout and no 'stop' between 'start' - // and 'centralManagerDidUpdateStatus': - delegate->LEnotSupported(); + if (internalState == InquiryActive) { + if (scanTimer.elapsed() >= qt_LE_deviceInquiryLength() * 1000) { + // We indeed stop now: + [manager stopScan]; + [manager setDelegate:nil]; + internalState = InquiryFinished; + state.store(int(internalState)); + } else { + dispatch_queue_t leQueue(qt_LE_queue()); + Q_ASSERT(leQueue); + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, + int64_t(qt_LE_deviceInquiryLength() / 100. * NSEC_PER_SEC)), + leQueue, + ^{ + [self stopScan]; + }); + } } } -- (bool)start +- (void)handlePoweredOff { - Q_ASSERT_X(![self isActive], Q_FUNC_INFO, "LE device scan is already active"); - Q_ASSERT_X(delegate, Q_FUNC_INFO, "invalid delegate (null)"); - - if (!peripherals) { - qCCritical(QT_BT_OSX) << Q_FUNC_INFO << "internal error"; - return false; - } - - cancelled = false; - [peripherals removeAllObjects]; + // This is interesting on iOS only, where + // the system shows an alert asking to enable + // Bluetooth in the 'Settings' app. If not done yet (after 30 + // seconds) - we consider it an error. + if (internalState == InquiryStarting) { + if (errorTimer.elapsed() >= 30000) { + [manager setDelegate:nil]; + internalState = ErrorPoweredOff; + state.store(int(internalState)); + } else { + dispatch_queue_t leQueue(OSXBluetooth::qt_LE_queue()); + Q_ASSERT(leQueue); + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, + (int64_t)(30 / 100. * NSEC_PER_SEC)), + leQueue, + ^{ + [self handlePoweredOff]; + }); - if (manager) { - // We can never be here, if status was not updated yet. - [manager setDelegate:nil]; - [manager release]; + } } +} - startTime = QTime(); - scanPhase = startingScan; - manager = [CBCentralManager alloc]; - manager = [manager initWithDelegate:self queue:nil]; - if (!manager) { - qCCritical(QT_BT_OSX) << Q_FUNC_INFO << "failed to create a central manager"; - return false; - } +- (void)start +{ + dispatch_queue_t leQueue(OSXBluetooth::qt_LE_queue()); - return true; + Q_ASSERT(leQueue); + manager.reset([[CBCentralManager alloc] initWithDelegate:self queue:leQueue]); } - (void)centralManagerDidUpdateState:(CBCentralManager *)central { - Q_ASSERT_X(delegate, Q_FUNC_INFO, "invalid delegate (null)"); - - const CBCentralManagerState state = central.state; - - if (scanPhase == startingScan && (state == CBCentralManagerStatePoweredOn - || state == CBCentralManagerStateUnsupported - || state == CBCentralManagerStateUnauthorized - || state == CBCentralManagerStatePoweredOff)) { - // We probably had 'PoweredOff' before, - // cancel the previous handlePoweredOffAfterDelay. - [NSObject cancelPreviousPerformRequestsWithTarget:self]; - } + if (central != manager) + return; - if (cancelled) { - Q_ASSERT_X(scanPhase != activeScan, Q_FUNC_INFO, "in 'activeScan' phase"); - scanPhase = noActivity; - delegate->LEdeviceInquiryFinished(); + if (internalState != InquiryActive && internalState != InquiryStarting) return; - } - if (state == CBCentralManagerStatePoweredOn) { - if (scanPhase == startingScan) { - scanPhase = activeScan; -#ifndef Q_OS_OSX - const NSTimeInterval timeout([QT_MANGLE_NAMESPACE(OSXBTLEDeviceInquiry) inquiryLength] / 1000); - Q_ASSERT_X(timeout > 0., Q_FUNC_INFO, "invalid scan timeout"); - [self performSelector:@selector(stopScan) withObject:nil afterDelay:timeout]; -#endif - startTime = QTime::currentTime(); + using namespace OSXBluetooth; + + dispatch_queue_t leQueue(qt_LE_queue()); + Q_ASSERT(leQueue); + + const CBCentralManagerState cbState(central.state); + if (cbState == CBCentralManagerStatePoweredOn) { + if (internalState == InquiryStarting) { + internalState = InquiryActive; + // Scan time is actually 10 seconds. Having a block with such delay can prevent + // 'self' from being deleted in time, which is not good. So we split this + // 10 s. timeout into smaller 'chunks'. + scanTimer.start(); + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, + int64_t(qt_LE_deviceInquiryLength() / 100. * NSEC_PER_SEC)), + leQueue, + ^{ + [self stopScan]; + }); [manager scanForPeripheralsWithServices:nil options:nil]; } // Else we ignore. } else if (state == CBCentralManagerStateUnsupported || state == CBCentralManagerStateUnauthorized) { - if (scanPhase == startingScan) { - scanPhase = noActivity; - delegate->LEnotSupported(); - } else if (scanPhase == activeScan) { - // Cancel stopScan: - [NSObject cancelPreviousPerformRequestsWithTarget:self]; - - scanPhase = noActivity; + if (internalState == InquiryActive) { [manager stopScan]; - delegate->LEdeviceInquiryError(QBluetoothDeviceDiscoveryAgent::PoweredOffError); + // Not sure how this is possible at all, probably, can never happen. + internalState = ErrorPoweredOff; + } else { + internalState = ErrorLENotSupported; } - } else if (state == CBCentralManagerStatePoweredOff) { - if (scanPhase == startingScan) { + + [manager setDelegate:nil]; + } else if (cbState == CBCentralManagerStatePoweredOff) { + if (internalState == InquiryStarting) { #ifndef Q_OS_OSX // On iOS a user can see at this point an alert asking to enable // Bluetooth in the "Settings" app. If a user does, // we'll receive 'PoweredOn' state update later. - [self performSelector:@selector(handlePoweredOffAfterDelay) withObject:nil afterDelay:30.]; + // No change in state. Wait for 30 seconds (we split it into 'chunks' not + // to retain 'self' for too long ) ... + errorTimer.start(); + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, + (int64_t)(30 / 100. * NSEC_PER_SEC)), + leQueue, + ^{ + [self handlePoweredOff]; + }); return; #endif - scanPhase = noActivity; - delegate->LEnotSupported(); - } else if (scanPhase == activeScan) { - // Cancel stopScan: - [NSObject cancelPreviousPerformRequestsWithTarget:self]; - - scanPhase = noActivity; + internalState = ErrorPoweredOff; + } else { + internalState = ErrorPoweredOff; [manager stopScan]; - delegate->LEdeviceInquiryError(QBluetoothDeviceDiscoveryAgent::PoweredOffError); - } // Else we ignore. + } + + [manager setDelegate:nil]; } else { // The following two states we ignore (from Apple's docs): //" @@ -294,44 +264,36 @@ using namespace QT_NAMESPACE; // -CBCentralManagerStateResetting // The connection with the system service was momentarily // lost; an update is imminent. " + // Wait for this imminent update. } + + state.store(int(internalState)); } - (void)stop { - if (scanPhase != startingScan) { - // startingScan means either no selector at all, - // or handlePoweredOffAfterDelay and we do not want to cancel it yet, - // waiting for DidUpdateState or handlePoweredOffAfterDelay, whoever - // fires first ... - [NSObject cancelPreviousPerformRequestsWithTarget:self]; - } - - if (scanPhase == startingScan || cancelled) { - // We have to wait for a status update or handlePoweredOffAfterDelay. - cancelled = true; - return; - } - - if (scanPhase == activeScan) { + if (internalState == InquiryActive) [manager stopScan]; - scanPhase = noActivity; - delegate->LEdeviceInquiryFinished(); - } + + [manager setDelegate:nil]; + internalState = InquiryCancelled; + state.store(int(internalState)); } - (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI { - Q_UNUSED(central) - Q_UNUSED(advertisementData) + Q_UNUSED(advertisementData); using namespace OSXBluetooth; - if (scanPhase != activeScan) + if (central != manager) return; - Q_ASSERT_X(delegate, Q_FUNC_INFO, "invalid delegate (null)"); + if (internalState != InquiryActive) + return; + + QBluetoothUuid deviceUuid; #if QT_MAC_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_9, __IPHONE_7_0) if (QSysInfo::MacintoshVersion >= qt_OS_limit(QSysInfo::MV_10_9, QSysInfo::MV_IOS_7_0)) { @@ -340,45 +302,64 @@ using namespace QT_NAMESPACE; return; } - if (![peripherals objectForKey:peripheral.identifier]) { - [peripherals setObject:peripheral forKey:peripheral.identifier]; - const QBluetoothUuid deviceUuid(OSXBluetooth::qt_uuid(peripheral.identifier)); - delegate->LEdeviceFound(peripheral, deviceUuid, advertisementData, RSSI); + if ([uuids containsObject:peripheral.identifier]) { + // We already know this peripheral ... + return; } - return; + + [uuids addObject:peripheral.identifier]; + deviceUuid = OSXBluetooth::qt_uuid(peripheral.identifier); } #endif // Either SDK or the target is below 10.9/7.0: // The property UUID was finally removed in iOS 9, we have // to avoid compilation errors ... - CFUUIDRef cfUUID = Q_NULLPTR; + if (deviceUuid.isNull()) { + CFUUIDRef cfUUID = Q_NULLPTR; + + if ([peripheral respondsToSelector:@selector(UUID)]) { + // This will require a bridged cast if we switch to ARC ... + cfUUID = reinterpret_cast<CFUUIDRef>([peripheral performSelector:@selector(UUID)]); + } - if ([peripheral respondsToSelector:@selector(UUID)]) { - // This will require a bridged cast if we switch to ARC ... - cfUUID = reinterpret_cast<CFUUIDRef>([peripheral performSelector:@selector(UUID)]); + if (!cfUUID) { + qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "peripheral without CFUUID"; + return; + } + + StringStrongReference key(uuid_as_nsstring(cfUUID)); + if ([uuids containsObject:key.data()]) + return; // We've seen this peripheral before ... + [uuids addObject:key.data()]; + deviceUuid = OSXBluetooth::qt_uuid(cfUUID); } - if (!cfUUID) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "peripheral without CFUUID"; + if (deviceUuid.isNull()) { + qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "no way to address peripheral, QBluetoothUuid is null"; return; } - StringStrongReference key(uuid_as_nsstring(cfUUID)); - if (![peripherals objectForKey:key.data()]) { - [peripherals setObject:peripheral forKey:key.data()]; - const QBluetoothUuid deviceUuid(OSXBluetooth::qt_uuid(cfUUID)); - delegate->LEdeviceFound(peripheral, deviceUuid, advertisementData, RSSI); - } + QString name; + if (peripheral.name) + name = QString::fromNSString(peripheral.name); + + // TODO: fix 'classOfDevice' (0 for now). + QBluetoothDeviceInfo newDeviceInfo(deviceUuid, name, 0); + if (RSSI) + newDeviceInfo.setRssi([RSSI shortValue]); + // CoreBluetooth scans only for LE devices. + newDeviceInfo.setCoreConfigurations(QBluetoothDeviceInfo::LowEnergyCoreConfiguration); + devices.append(newDeviceInfo); } -- (bool)isActive +- (LEInquiryState) inquiryState { - return scanPhase == startingScan || scanPhase == activeScan; + return LEInquiryState(state.load()); } -- (const QTime&)startTime +- (const QList<QBluetoothDeviceInfo> &)discoveredDevices { - return startTime; + return devices; } @end diff --git a/src/bluetooth/osx/osxbtledeviceinquiry_p.h b/src/bluetooth/osx/osxbtledeviceinquiry_p.h index cb86cd14..9ca299ea 100644 --- a/src/bluetooth/osx/osxbtledeviceinquiry_p.h +++ b/src/bluetooth/osx/osxbtledeviceinquiry_p.h @@ -46,80 +46,59 @@ // #include "qbluetoothdevicediscoveryagent.h" +#include "qbluetoothdeviceinfo.h" +#include "osxbtutility_p.h" -#include <QtCore/qdatetime.h> +#include <QtCore/qelapsedtimer.h> #include <QtCore/qglobal.h> +#include <QtCore/qatomic.h> #include <QtCore/qlist.h> #include <Foundation/Foundation.h> -@class QT_MANGLE_NAMESPACE(OSXBTLEDeviceInquiry); - @class CBCentralManager; @class CBPeripheral; QT_BEGIN_NAMESPACE -class QBluetoothDeviceInfo; class QBluetoothUuid; -namespace OSXBluetooth { - -class LEDeviceInquiryDelegate -{ -public: - typedef QT_MANGLE_NAMESPACE(OSXBTLEDeviceInquiry) LEDeviceInquiryObjC; - - virtual ~LEDeviceInquiryDelegate(); - - // At the moment the only error we're reporting is PoweredOffError! - virtual void LEdeviceInquiryError(QBluetoothDeviceDiscoveryAgent::Error error) = 0; - - virtual void LEnotSupported() = 0; - virtual void LEdeviceFound(CBPeripheral *peripheral, const QBluetoothUuid &uuid, - NSDictionary *advertisementData, NSNumber *RSSI) = 0; - virtual void LEdeviceInquiryFinished() = 0; -}; - -} - QT_END_NAMESPACE -// Bluetooth Low Energy scan for iOS and OS X. -// Strong enum would be quite handy ... -enum LEScanPhase +enum LEInquiryState { - noActivity, - startingScan, - activeScan + InquiryStarting, + InquiryActive, + InquiryFinished, + InquiryCancelled, + ErrorPoweredOff, + ErrorLENotSupported }; @interface QT_MANGLE_NAMESPACE(OSXBTLEDeviceInquiry) : NSObject -{// Protocols are adopted in the mm file. - QT_PREPEND_NAMESPACE(OSXBluetooth)::LEDeviceInquiryDelegate *delegate; +{ + QT_PREPEND_NAMESPACE(OSXBluetooth)::ObjCScopedPointer<NSMutableSet> uuids; + QT_PREPEND_NAMESPACE(OSXBluetooth)::ObjCScopedPointer<CBCentralManager> manager; - // TODO: scoped pointers/shared pointers? - NSMutableDictionary *peripherals; // Found devices. - CBCentralManager *manager; + QList<QBluetoothDeviceInfo> devices; - LEScanPhase scanPhase; - bool cancelled; - QTime startTime; -} + LEInquiryState internalState; + QT_PREPEND_NAMESPACE(QAtomicInt) state; -// Inquiry length in milliseconds. -+ (int)inquiryLength; + // Timers to check if we can execute delayed callbacks: + QT_PREPEND_NAMESPACE(QElapsedTimer) errorTimer; + QT_PREPEND_NAMESPACE(QElapsedTimer) scanTimer; +} -- (id)initWithDelegate:(QT_PREPEND_NAMESPACE(OSXBluetooth)::LEDeviceInquiryDelegate *)aDelegate; +- (id)init; - (void)dealloc; -// Actual scan can be delayed - we have to wait for a status update first. -- (bool)start; -// Stop can be delayed - if we're waiting for a status update. +// IMPORTANT: both 'start' and 'stop' are to be executed on the "Qt's LE queue". +- (void)start; - (void)stop; -- (bool)isActive; -- (const QTime &)startTime; +- (LEInquiryState)inquiryState; +- (const QList<QBluetoothDeviceInfo> &)discoveredDevices; @end diff --git a/src/bluetooth/osx/osxbtutility.mm b/src/bluetooth/osx/osxbtutility.mm index a5d3d936..08ff699d 100644 --- a/src/bluetooth/osx/osxbtutility.mm +++ b/src/bluetooth/osx/osxbtutility.mm @@ -305,6 +305,47 @@ ObjCStrongReference<NSData> data_from_bytearray(const QByteArray & qtData) return result; } +// A small RAII class for a dispatch queue. +class SerialDispatchQueue +{ +public: + explicit SerialDispatchQueue(const char *label) + { + Q_ASSERT(label); + + queue = dispatch_queue_create(label, DISPATCH_QUEUE_SERIAL); + if (!queue) { + qCCritical(QT_BT_OSX) << "failed to create dispatch queue with label" + << label; + } + } + ~SerialDispatchQueue() + { + if (queue) + dispatch_release(queue); + } + + dispatch_queue_t data() const + { + return queue; + } +private: + dispatch_queue_t queue; + + Q_DISABLE_COPY(SerialDispatchQueue) +}; + +dispatch_queue_t qt_LE_queue() +{ + static const SerialDispatchQueue leQueue("qt-bluetooth-LE-queue"); + return leQueue.data(); +} + +unsigned qt_LE_deviceInquiryLength() +{ + return 10; +} + } QT_END_NAMESPACE diff --git a/src/bluetooth/osx/osxbtutility_p.h b/src/bluetooth/osx/osxbtutility_p.h index a69e05c2..3506b0d1 100644 --- a/src/bluetooth/osx/osxbtutility_p.h +++ b/src/bluetooth/osx/osxbtutility_p.h @@ -306,6 +306,10 @@ inline QSysInfo::MacVersion qt_OS_limit(QSysInfo::MacVersion osxVersion, QSysInf #endif } +dispatch_queue_t qt_LE_queue(); +// LE scan, in seconds. +unsigned qt_LE_deviceInquiryLength(); + } // namespace OSXBluetooth // Logging category for both OS X and iOS. diff --git a/src/bluetooth/qbluetoothdevicediscoveryagent_ios.mm b/src/bluetooth/qbluetoothdevicediscoveryagent_ios.mm index 1556c5f9..30a6acb6 100644 --- a/src/bluetooth/qbluetoothdevicediscoveryagent_ios.mm +++ b/src/bluetooth/qbluetoothdevicediscoveryagent_ios.mm @@ -31,6 +31,7 @@ ** ****************************************************************************/ +#include "qbluetoothdevicediscoverytimer_osx_p.h" #include "qbluetoothdevicediscoveryagent.h" #include "osx/osxbtledeviceinquiry_p.h" #include "qbluetoothlocaldevice.h" @@ -51,27 +52,29 @@ QT_BEGIN_NAMESPACE using OSXBluetooth::ObjCScopedPointer; -class QBluetoothDeviceDiscoveryAgentPrivate : public OSXBluetooth::LEDeviceInquiryDelegate +class QBluetoothDeviceDiscoveryAgentPrivate { friend class QBluetoothDeviceDiscoveryAgent; + friend class OSXBluetooth::DDATimerHandler; + public: QBluetoothDeviceDiscoveryAgentPrivate(const QBluetoothAddress &address, QBluetoothDeviceDiscoveryAgent *q); virtual ~QBluetoothDeviceDiscoveryAgentPrivate(); - bool isValid() const; bool isActive() const; void start(); void stop(); private: - // LEDeviceInquiryDelegate: - void LEdeviceInquiryError(QBluetoothDeviceDiscoveryAgent::Error error) Q_DECL_OVERRIDE; - void LEnotSupported() Q_DECL_OVERRIDE; - void LEdeviceFound(CBPeripheral *peripheral, const QBluetoothUuid &deviceUuid, - NSDictionary *advertisementData, NSNumber *RSSI) Q_DECL_OVERRIDE; - void LEdeviceInquiryFinished() Q_DECL_OVERRIDE; + typedef QT_MANGLE_NAMESPACE(OSXBTLEDeviceInquiry) LEDeviceInquiryObjC; + + void LEinquiryError(QBluetoothDeviceDiscoveryAgent::Error error); + void LEnotSupported(); + void LEdeviceFound(const QBluetoothDeviceInfo &info); + void LEinquiryFinished(); + void checkLETimeout(); void setError(QBluetoothDeviceDiscoveryAgent::Error, const QString &text = QString()); @@ -90,8 +93,45 @@ private: bool startPending; bool stopPending; + + QScopedPointer<OSXBluetooth::DDATimerHandler> timer; }; +namespace OSXBluetooth { + +DDATimerHandler::DDATimerHandler(QBluetoothDeviceDiscoveryAgentPrivate *d) + : owner(d) +{ + Q_ASSERT_X(owner, Q_FUNC_INFO, "invalid pointer"); + + timer.setSingleShot(false); + connect(&timer, &QTimer::timeout, this, &DDATimerHandler::onTimer); +} + +void DDATimerHandler::start(int msec) +{ + Q_ASSERT_X(msec > 0, Q_FUNC_INFO, "invalid time interval"); + if (timer.isActive()) { + qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "timer is active"; + return; + } + + timer.start(msec); +} + +void DDATimerHandler::stop() +{ + timer.stop(); +} + +void DDATimerHandler::onTimer() +{ + Q_ASSERT(owner); + owner->checkLETimeout(); +} + +} + QBluetoothDeviceDiscoveryAgentPrivate::QBluetoothDeviceDiscoveryAgentPrivate(const QBluetoothAddress &adapter, QBluetoothDeviceDiscoveryAgent *q) : q_ptr(q), @@ -103,29 +143,20 @@ QBluetoothDeviceDiscoveryAgentPrivate::QBluetoothDeviceDiscoveryAgentPrivate(con Q_UNUSED(adapter); Q_ASSERT_X(q != Q_NULLPTR, Q_FUNC_INFO, "invalid q_ptr (null)"); - - // OSXBTLEDeviceInquiry can be constructed even if LE is not supported - - // at this stage it's only a memory allocation of the object itself, - // if it fails - we have some memory-related problems. - LEDeviceInquiry newInquiryLE([[LEDeviceInquiryObjC alloc] initWithDelegate:this]); - if (!newInquiryLE) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "failed to initialize a device inquiry object"; - return; - } - - inquiryLE.reset(newInquiryLE.take()); } QBluetoothDeviceDiscoveryAgentPrivate::~QBluetoothDeviceDiscoveryAgentPrivate() { -} - -bool QBluetoothDeviceDiscoveryAgentPrivate::isValid() const -{ - // isValid() - Qt does not use exceptions, but the ctor - // can fail to initialize some important data-members - // - this is what meant here by valid/invalid. - return inquiryLE; + if (inquiryLE) { + // 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 ... + LEDeviceInquiryObjC *inq = inquiryLE.data(); + dispatch_async(leQueue, ^{ + [inq stop]; + }); + } + } } bool QBluetoothDeviceDiscoveryAgentPrivate::isActive() const @@ -135,50 +166,66 @@ bool QBluetoothDeviceDiscoveryAgentPrivate::isActive() const if (stopPending) return false; - return [inquiryLE isActive]; + return inquiryLE; } void QBluetoothDeviceDiscoveryAgentPrivate::start() { - Q_ASSERT_X(isValid(), Q_FUNC_INFO, "called on invalid device discovery agent"); Q_ASSERT_X(!isActive(), Q_FUNC_INFO, "called on active device discovery agent"); - Q_ASSERT_X(lastError != QBluetoothDeviceDiscoveryAgent::InvalidBluetoothAdapterError, - Q_FUNC_INFO, "called with an invalid Bluetooth adapter"); if (stopPending) { startPending = true; return; } - discoveredDevices.clear(); - setError(QBluetoothDeviceDiscoveryAgent::NoError); + using namespace OSXBluetooth; - if (![inquiryLE start]) { - // We can be here only if we have some kind of - // resource allocation error. + inquiryLE.reset([[LEDeviceInquiryObjC alloc] init]); + dispatch_queue_t leQueue(qt_LE_queue()); + if (!leQueue || !inquiryLE) { setError(QBluetoothDeviceDiscoveryAgent::UnknownError, QCoreApplication::translate(DEV_DISCOVERY, DD_NOT_STARTED)); emit q_ptr->error(lastError); } + + discoveredDevices.clear(); + setError(QBluetoothDeviceDiscoveryAgent::NoError); + + // CoreBluetooth does not have a timeout. We start a timer here + // and check if scan really started and if yes if we have a timeout. + timer.reset(new OSXBluetooth::DDATimerHandler(this)); + timer->start(2000); + + // Create a local variable - to have a strong referece in a block. + LEDeviceInquiryObjC *inq = inquiryLE.data(); + dispatch_async(leQueue, ^{ + [inq start]; + }); } void QBluetoothDeviceDiscoveryAgentPrivate::stop() { - Q_ASSERT_X(isValid(), Q_FUNC_INFO, "called on invalid device discovery agent"); 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"); startPending = false; stopPending = true; + dispatch_queue_t leQueue(OSXBluetooth::qt_LE_queue()); + Q_ASSERT(leQueue); + setError(QBluetoothDeviceDiscoveryAgent::NoError); - // Can be asynchronous (depending on a status update of CBCentralManager). - // The call itself is always 'success'. - [inquiryLE stop]; + + // Create a local variable - to have a strong referece in a block. + LEDeviceInquiryObjC *inq = inquiryLE.data(); + dispatch_async(leQueue, ^{ + [inq stop]; + }); + // We consider LE scan to be stopped immediately and + // do not care about this LEDeviceInquiry object anymore. + LEinquiryFinished(); } -void QBluetoothDeviceDiscoveryAgentPrivate::LEdeviceInquiryError(QBluetoothDeviceDiscoveryAgent::Error error) +void QBluetoothDeviceDiscoveryAgentPrivate::LEinquiryError(QBluetoothDeviceDiscoveryAgent::Error error) { // At the moment the only error reported by osxbtledeviceinquiry // can be 'powered off' error, it happens @@ -187,6 +234,9 @@ void QBluetoothDeviceDiscoveryAgentPrivate::LEdeviceInquiryError(QBluetoothDevic Q_ASSERT_X(error == QBluetoothDeviceDiscoveryAgent::PoweredOffError, Q_FUNC_INFO, "unexpected error"); + inquiryLE.reset(); + timer->stop(); + startPending = false; stopPending = false; setError(error); @@ -195,36 +245,17 @@ void QBluetoothDeviceDiscoveryAgentPrivate::LEdeviceInquiryError(QBluetoothDevic void QBluetoothDeviceDiscoveryAgentPrivate::LEnotSupported() { + inquiryLE.reset(); + timer->stop(); + startPending = false; stopPending = false; setError(QBluetoothDeviceDiscoveryAgent::UnsupportedPlatformError); emit q_ptr->error(lastError); } -void QBluetoothDeviceDiscoveryAgentPrivate::LEdeviceFound(CBPeripheral *peripheral, const QBluetoothUuid &deviceUuid, - NSDictionary *advertisementData, - NSNumber *RSSI) +void QBluetoothDeviceDiscoveryAgentPrivate::LEdeviceFound(const QBluetoothDeviceInfo &newDeviceInfo) { - Q_ASSERT_X(peripheral, Q_FUNC_INFO, "invalid peripheral (nil)"); - - QT_BT_MAC_AUTORELEASEPOOL; - - QString name; - if (peripheral.name && peripheral.name.length) { - name = QString::fromNSString(peripheral.name); - } else { - NSString *const localName = [advertisementData objectForKey:CBAdvertisementDataLocalNameKey]; - if (localName && [localName length]) - name = QString::fromNSString(localName); - } - - // TODO: fix 'classOfDevice' (0 for now). - QBluetoothDeviceInfo newDeviceInfo(deviceUuid, name, 0); - if (RSSI) - newDeviceInfo.setRssi([RSSI shortValue]); - // CoreBluetooth scans only for LE devices. - newDeviceInfo.setCoreConfigurations(QBluetoothDeviceInfo::LowEnergyCoreConfiguration); - // Update, append or discard. for (int i = 0, e = discoveredDevices.size(); i < e; ++i) { if (discoveredDevices[i].deviceUuid() == newDeviceInfo.deviceUuid()) { @@ -241,9 +272,10 @@ void QBluetoothDeviceDiscoveryAgentPrivate::LEdeviceFound(CBPeripheral *peripher emit q_ptr->deviceDiscovered(newDeviceInfo); } -void QBluetoothDeviceDiscoveryAgentPrivate::LEdeviceInquiryFinished() +void QBluetoothDeviceDiscoveryAgentPrivate::LEinquiryFinished() { - Q_ASSERT_X(isValid(), Q_FUNC_INFO, "invalid device discovery agent"); + inquiryLE.reset(); + timer->stop(); if (stopPending && !startPending) { stopPending = false; @@ -257,6 +289,41 @@ void QBluetoothDeviceDiscoveryAgentPrivate::LEdeviceInquiryFinished() } } +void QBluetoothDeviceDiscoveryAgentPrivate::checkLETimeout() +{ + Q_ASSERT_X(inquiryLE, Q_FUNC_INFO, "LE device inquiry is nil"); + + using namespace OSXBluetooth; + + const LEInquiryState state([inquiryLE inquiryState]); + if (state == InquiryStarting || state == InquiryActive) + return; // Wait ... + + if (state == ErrorPoweredOff) + return LEinquiryError(QBluetoothDeviceDiscoveryAgent::PoweredOffError); + + if (state == ErrorLENotSupported) + return LEnotSupported(); + + if (state == InquiryFinished) { + // Process found devices if any ... + const QList<QBluetoothDeviceInfo> leDevices([inquiryLE discoveredDevices]); + foreach (const QBluetoothDeviceInfo &info, leDevices) { + // We were cancelled on a previous device discovered signal ... + if (!inquiryLE) + break; + LEdeviceFound(info); + } + + if (inquiryLE) + LEinquiryFinished(); + return; + } + + qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "unexpected inquiry state in LE timeout"; + // Actually, this deserves an assert :) +} + void QBluetoothDeviceDiscoveryAgentPrivate::setError(QBluetoothDeviceDiscoveryAgent::Error error, const QString &text) { @@ -329,36 +396,22 @@ QList<QBluetoothDeviceInfo> QBluetoothDeviceDiscoveryAgent::discoveredDevices() void QBluetoothDeviceDiscoveryAgent::start() { if (d_ptr->lastError != InvalidBluetoothAdapterError) { - if (d_ptr->isValid()) { - if (!isActive()) - d_ptr->start(); - else - qCDebug(QT_BT_OSX) << Q_FUNC_INFO << "already started"; - } else { - // We previously failed to initialize - // private object correctly. - d_ptr->setError(InvalidBluetoothAdapterError); - emit error(InvalidBluetoothAdapterError); - } + if (!isActive()) + d_ptr->start(); + else + qCDebug(QT_BT_OSX) << Q_FUNC_INFO << "already started"; } } void QBluetoothDeviceDiscoveryAgent::stop() { - if (d_ptr->isValid()) { - if (isActive() && d_ptr->lastError != InvalidBluetoothAdapterError) - d_ptr->stop(); - else - qCDebug(QT_BT_OSX) << Q_FUNC_INFO << "failed to stop"; - } + if (isActive() && d_ptr->lastError != InvalidBluetoothAdapterError) + d_ptr->stop(); } bool QBluetoothDeviceDiscoveryAgent::isActive() const { - if (d_ptr->isValid()) - return d_ptr->isActive(); - - return false; + return d_ptr->isActive(); } QBluetoothDeviceDiscoveryAgent::Error QBluetoothDeviceDiscoveryAgent::error() const diff --git a/src/bluetooth/qbluetoothdevicediscoveryagent_osx.mm b/src/bluetooth/qbluetoothdevicediscoveryagent_osx.mm index ad4183a7..1cfe8286 100644 --- a/src/bluetooth/qbluetoothdevicediscoveryagent_osx.mm +++ b/src/bluetooth/qbluetoothdevicediscoveryagent_osx.mm @@ -59,15 +59,16 @@ QT_BEGIN_NAMESPACE using OSXBluetooth::ObjCScopedPointer; -class QBluetoothDeviceDiscoveryAgentPrivate : public OSXBluetooth::DeviceInquiryDelegate, - public OSXBluetooth::LEDeviceInquiryDelegate +class QBluetoothDeviceDiscoveryAgentPrivate : public OSXBluetooth::DeviceInquiryDelegate { friend class QBluetoothDeviceDiscoveryAgent; friend class OSXBluetooth::DDATimerHandler; public: + typedef QT_MANGLE_NAMESPACE(OSXBTLEDeviceInquiry) LEDeviceInquiryObjC; + QBluetoothDeviceDiscoveryAgentPrivate(const QBluetoothAddress & address, QBluetoothDeviceDiscoveryAgent *q); - virtual ~QBluetoothDeviceDiscoveryAgentPrivate(); // Just to make compiler happy. + virtual ~QBluetoothDeviceDiscoveryAgentPrivate(); bool isValid() const; bool isActive() const; @@ -87,12 +88,11 @@ private: void inquiryFinished(IOBluetoothDeviceInquiry *inq) Q_DECL_OVERRIDE; void error(IOBluetoothDeviceInquiry *inq, IOReturn error) Q_DECL_OVERRIDE; void deviceFound(IOBluetoothDeviceInquiry *inq, IOBluetoothDevice *device) Q_DECL_OVERRIDE; - // LEDeviceInquiryDelegate: - void LEdeviceInquiryError(QBluetoothDeviceDiscoveryAgent::Error error) Q_DECL_OVERRIDE; - void LEnotSupported() Q_DECL_OVERRIDE; - void LEdeviceFound(CBPeripheral *peripheral, const QBluetoothUuid &deviceUuid, - NSDictionary *advertisementData, NSNumber *RSSI) Q_DECL_OVERRIDE; - void LEdeviceInquiryFinished() Q_DECL_OVERRIDE; + + // + void LEinquiryFinished(); + void LEinquiryError(QBluetoothDeviceDiscoveryAgent::Error error); + void LEnotSupported(); // Check if it's a really new device/updated info and emit // q_ptr->deviceDiscovered. @@ -138,7 +138,7 @@ DDATimerHandler::DDATimerHandler(QBluetoothDeviceDiscoveryAgentPrivate *d) { Q_ASSERT_X(owner, Q_FUNC_INFO, "invalid pointer"); - timer.setSingleShot(true); + timer.setSingleShot(false); connect(&timer, &QTimer::timeout, this, &DDATimerHandler::onTimer); } @@ -192,23 +192,22 @@ QBluetoothDeviceDiscoveryAgentPrivate::QBluetoothDeviceDiscoveryAgentPrivate(con return; } - // OSXBTLEDeviceInquiry can be constructed even if LE is not supported - - // at this stage it's only a memory allocation of the object itself, - // if it fails - we have some memory-related problem. - LEDeviceInquiry newInquiryLE([[LEDeviceInquiryObjC alloc] initWithDelegate:this]); - if (!newInquiryLE) { - qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "failed to " - "initialize a LE inquiry"; - return; - } - hostController.reset(controller.take()); inquiry.reset(newInquiry.take()); - inquiryLE.reset(newInquiryLE.take()); } 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 ... + LEDeviceInquiryObjC *inq = inquiryLE.data(); + dispatch_async(leQueue, ^{ + [inq stop]; + }); + } + } } bool QBluetoothDeviceDiscoveryAgentPrivate::isValid() const @@ -218,7 +217,7 @@ bool QBluetoothDeviceDiscoveryAgentPrivate::isValid() const // (and the error is probably not even related to Bluetooth at all) // - say, allocation error - this is what meant here by valid/invalid. return hostController && [hostController powerState] == kBluetoothHCIPowerStateON - && inquiry && inquiryLE; + && inquiry; } bool QBluetoothDeviceDiscoveryAgentPrivate::isActive() const @@ -263,22 +262,29 @@ void QBluetoothDeviceDiscoveryAgentPrivate::startLE() Q_ASSERT_X(lastError != QBluetoothDeviceDiscoveryAgent::InvalidBluetoothAdapterError, Q_FUNC_INFO, "called with an invalid Bluetooth adapter"); - agentState = LEScan; + using namespace OSXBluetooth; - // CoreBluetooth does not have a timeout. We start a timer here - // and check if scan really started and if yes if we have a timeout. - timer.reset(new OSXBluetooth::DDATimerHandler(this)); - timer->start([LEDeviceInquiryObjC inquiryLength]); + inquiryLE.reset([[LEDeviceInquiryObjC alloc] init]); - if (![inquiryLE start]) { - // We can be here only if we have some kind of resource allocation error, so we - // do not emit finished, we emit error. - timer->stop(); + 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); } + + agentState = LEScan; + // CoreBluetooth does not have a timeout. We start a timer here + // and check if scan is active/finished/finished with error(s). + timer.reset(new OSXBluetooth::DDATimerHandler(this)); + timer->start(2000); + + // We need the local variable so that it's retained ... + LEDeviceInquiryObjC *inq = inquiryLE.data(); + dispatch_async(leQueue, ^{ + [inq start]; + }); } void QBluetoothDeviceDiscoveryAgentPrivate::stop() @@ -288,6 +294,8 @@ void QBluetoothDeviceDiscoveryAgentPrivate::stop() 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; @@ -304,9 +312,16 @@ void QBluetoothDeviceDiscoveryAgentPrivate::stop() emit q_ptr->error(lastError); } } else { - // Can be asynchronous (depending on a status update of CBCentralManager). - // The call itself is always 'success'. - [inquiryLE stop]; + dispatch_queue_t leQueue(qt_LE_queue()); + Q_ASSERT(leQueue); + // We need the local variable so that it's retained ... + LEDeviceInquiryObjC *inq = inquiryLE.data(); + dispatch_async(leQueue, ^{ + [inq stop]; + }); + // We consider LE scan to be stopped immediately and + // do not care about this LEDeviceInquiry object anymore. + LEinquiryFinished(); } } @@ -430,31 +445,45 @@ void QBluetoothDeviceDiscoveryAgentPrivate::checkLETimeout() Q_ASSERT_X(agentState == LEScan, Q_FUNC_INFO, "invalid agent state"); Q_ASSERT_X(inquiryLE, Q_FUNC_INFO, "LE device inquiry is nil"); - const int timeout = [LEDeviceInquiryObjC inquiryLength]; - Q_ASSERT(timeout > 0); - const QTime scanStartTime([inquiryLE startTime]); - if (scanStartTime.isValid()) { - const int elapsed = scanStartTime.msecsTo(QTime::currentTime()); - Q_ASSERT(elapsed >= 0); - if (elapsed >= timeout) - [inquiryLE stop]; - else - timer->start(timeout - elapsed); - } else { - // Scan not started yet. Wait 5 seconds more. - timer->start(timeout / 2); + using namespace OSXBluetooth; + + const LEInquiryState state([inquiryLE inquiryState]); + if (state == InquiryStarting || state == InquiryActive) + return; // Wait ... + + if (state == ErrorPoweredOff) + return LEinquiryError(QBluetoothDeviceDiscoveryAgent::PoweredOffError); + + if (state == ErrorLENotSupported) + return LEnotSupported(); + + if (state == InquiryFinished) { + // Process found devices if any ... + const QList<QBluetoothDeviceInfo> leDevices([inquiryLE discoveredDevices]); + foreach (const QBluetoothDeviceInfo &info, leDevices) { + // We were cancelled on a previous device discovered signal ... + if (agentState != LEScan) + break; + deviceFound(info); + } + + if (agentState == LEScan) + LEinquiryFinished(); + return; } + + qCWarning(QT_BT_OSX) << Q_FUNC_INFO << "unexpected inquiry state in LE timeout"; + // Actually, this deserves an assert :) } -void QBluetoothDeviceDiscoveryAgentPrivate::LEdeviceInquiryError(QBluetoothDeviceDiscoveryAgent::Error error) +void QBluetoothDeviceDiscoveryAgentPrivate::LEinquiryError(QBluetoothDeviceDiscoveryAgent::Error error) { // At the moment the only error reported can be 'powered off' error, it happens // after the LE scan started (so we have LE support and this is a real PoweredOffError). - Q_ASSERT_X(error == QBluetoothDeviceDiscoveryAgent::PoweredOffError, - Q_FUNC_INFO, "unexpected error code"); + Q_ASSERT(error == QBluetoothDeviceDiscoveryAgent::PoweredOffError); timer->stop(); - + inquiryLE.reset(); agentState = NonActive; setError(error); emit q_ptr->error(lastError); @@ -462,45 +491,16 @@ void QBluetoothDeviceDiscoveryAgentPrivate::LEdeviceInquiryError(QBluetoothDevic void QBluetoothDeviceDiscoveryAgentPrivate::LEnotSupported() { - // Not supported is not an error. + // Not supported is not an error (we still have 'Classic'). qCDebug(QT_BT_OSX) << "no Bluetooth LE support"; - // After we call startLE and before receive NotSupported, - // the user can call stop (setting a pending stop). - // So the same rule apply: - timer->stop(); - - LEdeviceInquiryFinished(); + LEinquiryFinished(); } -void QBluetoothDeviceDiscoveryAgentPrivate::LEdeviceFound(CBPeripheral *peripheral, const QBluetoothUuid &deviceUuid, - NSDictionary *advertisementData, - NSNumber *RSSI) -{ - Q_ASSERT_X(peripheral, Q_FUNC_INFO, "invalid peripheral (nil)"); - Q_ASSERT_X(agentState == LEScan, Q_FUNC_INFO, - "invalid agent state, expected LE scan"); - - Q_UNUSED(advertisementData) - - QString name; - if (peripheral.name) - name = QString::fromNSString(peripheral.name); - - // TODO: fix 'classOfDevice' (0 for now). - QBluetoothDeviceInfo newDeviceInfo(deviceUuid, name, 0); - if (RSSI) - newDeviceInfo.setRssi([RSSI shortValue]); - // CoreBluetooth scans only for LE devices. - newDeviceInfo.setCoreConfigurations(QBluetoothDeviceInfo::LowEnergyCoreConfiguration); - - deviceFound(newDeviceInfo); -} - -void QBluetoothDeviceDiscoveryAgentPrivate::LEdeviceInquiryFinished() +void QBluetoothDeviceDiscoveryAgentPrivate::LEinquiryFinished() { // The same logic as in inquiryFinished, but does not start LE scan. agentState = NonActive; - + inquiryLE.reset(); timer->stop(); if (stopPending && !startPending) { |