diff options
author | Alex Blasche <alexander.blasche@qt.io> | 2018-07-25 14:42:06 +0200 |
---|---|---|
committer | Alex Blasche <alexander.blasche@qt.io> | 2018-07-25 14:42:18 +0200 |
commit | 25638b02766ae3109bfc83b3249e6d0dc9e21bdb (patch) | |
tree | 8ac8f0cd544dd39da674657940d6c509ea94721c /src/bluetooth/osx | |
parent | fc2a206322f6190226ebc1f04062f2c9170f0bac (diff) | |
parent | b19148f9a0f820630bd83432d96117e9598c315d (diff) |
Merge remote-tracking branch 'gerrit/dev' into btlebtle
Change-Id: Id7698ec157a4e06296bcc27d48aaa8325dd3c23a
Diffstat (limited to 'src/bluetooth/osx')
-rw-r--r-- | src/bluetooth/osx/osxbt.pri | 7 | ||||
-rw-r--r-- | src/bluetooth/osx/osxbtcentralmanager.mm | 226 | ||||
-rw-r--r-- | src/bluetooth/osx/osxbtcentralmanager_p.h | 23 | ||||
-rw-r--r-- | src/bluetooth/osx/osxbtgcdtimer.mm | 111 | ||||
-rw-r--r-- | src/bluetooth/osx/osxbtgcdtimer_p.h | 94 | ||||
-rw-r--r-- | src/bluetooth/osx/osxbtledeviceinquiry.mm | 119 | ||||
-rw-r--r-- | src/bluetooth/osx/osxbtledeviceinquiry_p.h | 18 | ||||
-rw-r--r-- | src/bluetooth/osx/osxbtperipheralmanager.mm | 5 |
8 files changed, 466 insertions, 137 deletions
diff --git a/src/bluetooth/osx/osxbt.pri b/src/bluetooth/osx/osxbt.pri index 13187e4f..b7ac0535 100644 --- a/src/bluetooth/osx/osxbt.pri +++ b/src/bluetooth/osx/osxbt.pri @@ -1,6 +1,9 @@ SOURCES += osx/uistrings.cpp osx/osxbtnotifier.cpp -PRIVATE_HEADERS += osx/uistrings_p.h -//QMAKE_CXXFLAGS_WARN_ON += -Wno-nullability-completeness +PRIVATE_HEADERS += osx/uistrings_p.h \ + osx/osxbtgcdtimer_p.h + +OBJECTIVE_SOURCES += osx/osxbtgcdtimer.mm +#QMAKE_CXXFLAGS_WARN_ON += -Wno-nullability-completeness CONFIG(osx) { PRIVATE_HEADERS += osx/osxbtutility_p.h \ diff --git a/src/bluetooth/osx/osxbtcentralmanager.mm b/src/bluetooth/osx/osxbtcentralmanager.mm index 70473f1f..9254bd98 100644 --- a/src/bluetooth/osx/osxbtcentralmanager.mm +++ b/src/bluetooth/osx/osxbtcentralmanager.mm @@ -79,8 +79,21 @@ NSUInteger qt_countGATTEntries(CBService *service) return n; } +ObjCStrongReference<NSError> qt_timeoutNSError(OperationTimeout type) +{ + // For now we do not provide details, since nobody is using this NSError + // after all (except callbacks checking if an operation was successful + // or not). + Q_ASSERT(type != OperationTimeout::none); + Q_UNUSED(type) + NSError *nsError = [[NSError alloc] initWithDomain:CBErrorDomain + code:CBErrorOperationCancelled + userInfo:nil]; + return ObjCStrongReference<NSError>(nsError, false /*do not retain, done already*/); } +} // namespace OSXBluetooth + QT_END_NAMESPACE @@ -115,9 +128,11 @@ QT_END_NAMESPACE - (id)initWith:(OSXBluetooth::LECBManagerNotifier *)aNotifier { + using namespace OSXBluetooth; + if (self = [super init]) { manager = nil; - managerState = OSXBluetooth::CentralManagerIdle; + managerState = CentralManagerIdle; disconnectPending = false; peripheral = nil; notifier = aNotifier; @@ -125,6 +140,17 @@ QT_END_NAMESPACE lastValidHandle = 0; requestPending = false; currentReadHandle = 0; + timeoutType = OperationTimeout::none; + + if (Q_UNLIKELY(!qEnvironmentVariableIsEmpty("BLUETOOTH_GATT_TIMEOUT"))) { + bool ok = false; + const int value = qEnvironmentVariableIntValue("BLUETOOTH_GATT_TIMEOUT", &ok); + if (ok && value >= 0) + timeoutMS = value; + } + + if (!timeoutMS) + timeoutMS = 20000; } return self; @@ -153,6 +179,63 @@ QT_END_NAMESPACE [super dealloc]; } +- (void)watchAfter:(id)object timeout:(OSXBluetooth::OperationTimeout)type +{ + using namespace OSXBluetooth; + + objectUnderWatch = object; + timeoutType = type; + [timeoutWatchdog cancelTimer]; + timeoutWatchdog.reset([[GCDTimerObjC alloc] initWithDelegate:self]); + [timeoutWatchdog startWithTimeout:timeoutMS step:200]; +} + +- (void)stopWatchdog +{ + [timeoutWatchdog cancelTimer]; + objectUnderWatch = nil; + timeoutType = OSXBluetooth::OperationTimeout::none; +} + +- (void)timeout +{ + using namespace OSXBluetooth; + + Q_ASSERT(objectUnderWatch); + NSLog(@"Timeout caused by: %@", objectUnderWatch); + const ObjCStrongReference<NSError> nsError(qt_timeoutNSError(timeoutType)); + switch (timeoutType) { + case OperationTimeout::serviceDiscovery: + qCWarning(QT_BT_OSX, "Timeout in services discovery"); + [self peripheral:peripheral didDiscoverServices:nsError]; + break; + case OperationTimeout::includedServicesDiscovery: + qCWarning(QT_BT_OSX, "Timeout in included services discovery"); + [self peripheral:peripheral didDiscoverIncludedServicesForService:objectUnderWatch error:nsError]; + break; + case OperationTimeout::characteristicsDiscovery: + qCWarning(QT_BT_OSX, "Timeout in characteristics discovery"); + [self peripheral:peripheral didDiscoverCharacteristicsForService:objectUnderWatch error:nsError]; + break; + case OperationTimeout::characteristicRead: + qCWarning(QT_BT_OSX, "Timeout while reading a characteristic"); + [self peripheral:peripheral didUpdateValueForCharacteristic:objectUnderWatch error:nsError]; + break; + case OperationTimeout::descriptorsDiscovery: + qCWarning(QT_BT_OSX, "Timeout in descriptors discovery"); + [self peripheral:peripheral didDiscoverDescriptorsForCharacteristic:objectUnderWatch error:nsError]; + break; + case OperationTimeout::descriptorRead: + qCWarning(QT_BT_OSX, "Timeout while reading a descriptor"); + [self peripheral:peripheral didUpdateValueForDescriptor:objectUnderWatch error:nsError]; + break; + case OperationTimeout::characteristicWrite: + qCWarning(QT_BT_OSX, "Timeout while writing a characteristic with response"); + [self peripheral:peripheral didWriteValueForCharacteristic:objectUnderWatch error:nsError]; + default:; + } +} + - (void)connectToDevice:(const QBluetoothUuid &)aDeviceUuid { disconnectPending = false; // Cancel the previous disconnect if any. @@ -235,10 +318,11 @@ QT_END_NAMESPACE - (void)connectToPeripheral { + using namespace OSXBluetooth; + Q_ASSERT_X(manager, Q_FUNC_INFO, "invalid central manager (nil)"); Q_ASSERT_X(peripheral, Q_FUNC_INFO, "invalid peripheral (nil)"); - Q_ASSERT_X(managerState == OSXBluetooth::CentralManagerIdle, - Q_FUNC_INFO, "invalid state"); + Q_ASSERT_X(managerState == CentralManagerIdle, Q_FUNC_INFO, "invalid state"); // The state is still the same - connecting. if ([self isConnected]) { @@ -247,7 +331,7 @@ QT_END_NAMESPACE emit notifier->connected(); } else { qCDebug(QT_BT_OSX) << "trying to connect"; - managerState = OSXBluetooth::CentralManagerConnecting; + managerState = CentralManagerConnecting; [manager connectPeripheral:peripheral options:nil]; } } @@ -292,9 +376,10 @@ QT_END_NAMESPACE - (void)discoverServices { + using namespace OSXBluetooth; + Q_ASSERT_X(peripheral, Q_FUNC_INFO, "invalid peripheral (nil)"); - Q_ASSERT_X(managerState == OSXBluetooth::CentralManagerIdle, - Q_FUNC_INFO, "invalid state"); + Q_ASSERT_X(managerState == CentralManagerIdle, Q_FUNC_INFO, "invalid state"); // From Apple's docs: // @@ -304,7 +389,8 @@ QT_END_NAMESPACE // // ... but we'd like to have them all: [peripheral setDelegate:self]; - managerState = OSXBluetooth::CentralManagerDiscovering; + managerState = CentralManagerDiscovering; + [self watchAfter:peripheral timeout:OperationTimeout::serviceDiscovery]; [peripheral discoverServices:nil]; } @@ -333,6 +419,7 @@ QT_END_NAMESPACE CBService *const s = [services objectAtIndex:currentService]; [visitedServices addObject:s]; managerState = CentralManagerDiscovering; + [self watchAfter:s timeout:OperationTimeout::includedServicesDiscovery]; [peripheral discoverIncludedServices:nil forService:s]; } } @@ -359,6 +446,7 @@ QT_END_NAMESPACE if (CBService *const service = [self serviceForUUID:serviceUuid]) { servicesToDiscoverDetails.append(serviceUuid); + [self watchAfter:service timeout:OperationTimeout::characteristicsDiscovery]; [peripheral discoverCharacteristics:nil forService:service]; return; } @@ -366,10 +454,8 @@ QT_END_NAMESPACE qCWarning(QT_BT_OSX) << "unknown service uuid" << serviceUuid; - if (notifier) { - emit notifier->CBManagerError(serviceUuid, - QLowEnergyService::UnknownError); - } + if (notifier) + emit notifier->CBManagerError(serviceUuid, QLowEnergyService::UnknownError); } - (void)readCharacteristics:(CBService *)service @@ -391,8 +477,10 @@ QT_END_NAMESPACE NSArray *const cs = service.characteristics; for (CBCharacteristic *c in cs) { - if (c.properties & CBCharacteristicPropertyRead) + if (c.properties & CBCharacteristicPropertyRead) { + [self watchAfter:c timeout:OperationTimeout::characteristicRead]; return [peripheral readValueForCharacteristic:c]; + } } // No readable properties? Discover descriptors then: @@ -418,15 +506,18 @@ QT_END_NAMESPACE [self serviceDetailsDiscoveryFinished:service]; } else { // Start from 0 and continue in the callback. - [peripheral discoverDescriptorsForCharacteristic:[service.characteristics objectAtIndex:0]]; + CBCharacteristic *ch = [service.characteristics objectAtIndex:0]; + [self watchAfter:ch timeout:OperationTimeout::descriptorsDiscovery]; + [peripheral discoverDescriptorsForCharacteristic:ch]; } } - (void)readDescriptors:(CBService *)service { + using namespace OSXBluetooth; + Q_ASSERT_X(service, Q_FUNC_INFO, "invalid service (nil)"); - Q_ASSERT_X(managerState != OSXBluetooth::CentralManagerUpdating, - Q_FUNC_INFO, "invalid state"); + Q_ASSERT_X(managerState != CentralManagerUpdating, Q_FUNC_INFO, "invalid state"); Q_ASSERT_X(peripheral, Q_FUNC_INFO, "invalid peripheral (nil)"); QT_BT_MAC_AUTORELEASEPOOL; @@ -435,8 +526,11 @@ QT_END_NAMESPACE // We can never be here if we have no characteristics. Q_ASSERT_X(cs && cs.count, Q_FUNC_INFO, "invalid service"); for (CBCharacteristic *c in cs) { - if (c.descriptors && c.descriptors.count) - return [peripheral readValueForDescriptor:[c.descriptors objectAtIndex:0]]; + if (c.descriptors && c.descriptors.count) { + CBDescriptor *desc = [c.descriptors objectAtIndex:0]; + [self watchAfter:desc timeout:OperationTimeout::descriptorRead]; + return [peripheral readValueForDescriptor:desc]; + } } // No descriptors to read, done. @@ -571,6 +665,8 @@ QT_END_NAMESPACE requestPending = true; currentReadHandle = request.handle; + // Timeouts: for now, we do not alert timeoutWatchdog - never had such + // bug reports and after all a read timeout can be handled externally. [peripheral readValueForCharacteristic:charMap[request.handle]]; } else { if (!descMap.contains(request.handle)) { @@ -581,6 +677,7 @@ QT_END_NAMESPACE requestPending = true; currentReadHandle = request.handle; + // Timeouts: see the comment above (CharRead). [peripheral readValueForDescriptor:descMap[request.handle]]; } } @@ -657,6 +754,7 @@ QT_END_NAMESPACE return [self performNextRequest]; requestPending = true; + [self watchAfter:characteristic timeout:OperationTimeout::characteristicWrite]; [peripheral writeValue:data.data() forCharacteristic:characteristic type:CBCharacteristicWriteWithResponse]; } else { @@ -1021,6 +1119,7 @@ QT_END_NAMESPACE charMap.clear(); descMap.clear(); currentReadHandle = 0; + [self stopWatchdog]; // TODO: also serviceToVisit/VisitNext and visitedServices ? } @@ -1030,6 +1129,9 @@ QT_END_NAMESPACE { using namespace OSXBluetooth; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunguarded-availability-new" + const auto state = central.state; #if QT_IOS_PLATFORM_SDK_EQUAL_OR_ABOVE(__IPHONE_10_0) || QT_OSX_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_13) if (state == CBManagerStateUnknown @@ -1072,12 +1174,14 @@ QT_END_NAMESPACE #else if (state == CBCentralManagerStatePoweredOff) { #endif - managerState = CentralManagerIdle; + if (managerState == CentralManagerUpdating) { + managerState = CentralManagerIdle; // I've seen this instead of Unsupported on OS X. if (notifier) emit notifier->LEnotSupported(); } else { + managerState = CentralManagerIdle; // TODO: we need a better error + // what will happen if later the state changes to PoweredOn??? if (notifier) @@ -1099,6 +1203,8 @@ QT_END_NAMESPACE // We actually handled all known states, but .. Core Bluetooth can change? Q_ASSERT_X(0, Q_FUNC_INFO, "invalid centra's state"); } + +#pragma clang diagnostic pop } - (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)aPeripheral @@ -1162,10 +1268,16 @@ QT_END_NAMESPACE Q_UNUSED(aPeripheral) if (managerState != OSXBluetooth::CentralManagerDiscovering) { - // Canceled by -disconnectFromDevice. + // Canceled by -disconnectFromDevice, or as a result of a timeout. + return; + } + + if (objectUnderWatch != aPeripheral) { + // Timed out. return; } + [self stopWatchdog]; managerState = OSXBluetooth::CentralManagerIdle; if (error) { @@ -1173,9 +1285,9 @@ QT_END_NAMESPACE // TODO: better error mapping required. if (notifier) emit notifier->CBManagerError(QLowEnergyController::UnknownError); - } else { - [self discoverIncludedServices]; } + + [self discoverIncludedServices]; } @@ -1191,10 +1303,16 @@ QT_END_NAMESPACE return; } + if (service != objectUnderWatch) { + // Timed out. + return; + } + QT_BT_MAC_AUTORELEASEPOOL; Q_ASSERT_X(peripheral, Q_FUNC_INFO, "invalid peripheral (nil)"); + [self stopWatchdog]; managerState = CentralManagerIdle; if (error) { @@ -1217,6 +1335,7 @@ QT_END_NAMESPACE // Continue with discovery ... [visitedServices addObject:s]; managerState = CentralManagerDiscovering; + [self watchAfter:s timeout:OperationTimeout::includedServicesDiscovery]; return [peripheral discoverIncludedServices:nil forService:s]; } } @@ -1232,6 +1351,7 @@ QT_END_NAMESPACE if (![visitedServices containsObject:s]) { [visitedServices addObject:s]; managerState = CentralManagerDiscovering; + [self watchAfter:s timeout:OperationTimeout::includedServicesDiscovery]; return [peripheral discoverIncludedServices:nil forService:s]; } } @@ -1260,6 +1380,13 @@ QT_END_NAMESPACE return; } + if (service != objectUnderWatch) { + // Timed out already? + return; + } + + [self stopWatchdog]; + using namespace OSXBluetooth; Q_ASSERT_X(managerState != CentralManagerUpdating, Q_FUNC_INFO, "invalid state"); @@ -1269,9 +1396,9 @@ QT_END_NAMESPACE // We did not discover any characteristics and can not discover descriptors, // inform our delegate (it will set a service state also). emit notifier->CBManagerError(qt_uuid(service.UUID), QLowEnergyController::UnknownError); - } else { - [self readCharacteristics:service]; } + + [self readCharacteristics:service]; } - (void)peripheral:(CBPeripheral *)aPeripheral @@ -1285,6 +1412,10 @@ QT_END_NAMESPACE return; } + const bool readMatch = characteristic == objectUnderWatch; + if (readMatch) + [self stopWatchdog]; + using namespace OSXBluetooth; Q_ASSERT_X(managerState != CentralManagerUpdating, Q_FUNC_INFO, "invalid state"); @@ -1311,13 +1442,17 @@ QT_END_NAMESPACE } if (isDetailsDiscovery) { - // Test if we have any other characteristic to read yet. - CBCharacteristic *const next = [self nextCharacteristicForService:characteristic.service - startingFrom:characteristic properties:CBCharacteristicPropertyRead]; - if (next) - [peripheral readValueForCharacteristic:next]; - else - [self discoverDescriptors:characteristic.service]; + if (readMatch) { + // Test if we have any other characteristic to read yet. + CBCharacteristic *const next = [self nextCharacteristicForService:characteristic.service + startingFrom:characteristic properties:CBCharacteristicPropertyRead]; + if (next) { + [self watchAfter:next timeout:OperationTimeout::characteristicRead]; + [peripheral readValueForCharacteristic:next]; + } else { + [self discoverDescriptors:characteristic.service]; + } + } } else { // This is (probably) the result of update notification. // It's very possible we can have an invalid handle here (0) - @@ -1360,6 +1495,11 @@ QT_END_NAMESPACE QT_BT_MAC_AUTORELEASEPOOL; + if (characteristic != objectUnderWatch) + return; + + [self stopWatchdog]; + using namespace OSXBluetooth; if (error) { @@ -1370,10 +1510,12 @@ QT_END_NAMESPACE // Do we have more characteristics on this service to discover descriptors? CBCharacteristic *const next = [self nextCharacteristicForService:characteristic.service startingFrom:characteristic]; - if (next) + if (next) { + [self watchAfter:next timeout:OperationTimeout::descriptorsDiscovery]; [peripheral discoverDescriptorsForCharacteristic:next]; - else + } else { [self readDescriptors:characteristic.service]; + } } - (void)peripheral:(CBPeripheral *)aPeripheral @@ -1391,6 +1533,11 @@ QT_END_NAMESPACE QT_BT_MAC_AUTORELEASEPOOL; + if (descriptor != objectUnderWatch) + return; + + [self stopWatchdog]; + using namespace OSXBluetooth; CBService *const service = descriptor.characteristic.service; @@ -1417,6 +1564,7 @@ QT_END_NAMESPACE CBDescriptor *const next = [self nextDescriptorForCharacteristic:descriptor.characteristic startingFrom:descriptor]; if (next) { + [self watchAfter:next timeout:OperationTimeout::descriptorRead]; [peripheral readValueForDescriptor:next]; } else { // We either have to read a value for a next descriptor @@ -1426,8 +1574,11 @@ QT_END_NAMESPACE CBCharacteristic *nextCh = [self nextCharacteristicForService:ch.service startingFrom:ch]; while (nextCh) { - if (nextCh.descriptors && nextCh.descriptors.count) - return [peripheral readValueForDescriptor:[nextCh.descriptors objectAtIndex:0]]; + if (nextCh.descriptors && nextCh.descriptors.count) { + CBDescriptor *desc = [nextCh.descriptors objectAtIndex:0]; + [self watchAfter:desc timeout:OperationTimeout::descriptorRead]; + return [peripheral readValueForDescriptor:desc]; + } nextCh = [self nextCharacteristicForService:ch.service startingFrom:nextCh]; @@ -1474,6 +1625,10 @@ QT_END_NAMESPACE QT_BT_MAC_AUTORELEASEPOOL; + if (characteristic != objectUnderWatch) + return; + + [self stopWatchdog]; requestPending = false; @@ -1569,9 +1724,10 @@ QT_END_NAMESPACE if (notifier) { notifier->disconnect(); notifier->deleteLater(); - notifier = 0; + notifier = nullptr; } + [self stopWatchdog]; [self disconnectFromDevice]; } diff --git a/src/bluetooth/osx/osxbtcentralmanager_p.h b/src/bluetooth/osx/osxbtcentralmanager_p.h index 697d922c..e172d874 100644 --- a/src/bluetooth/osx/osxbtcentralmanager_p.h +++ b/src/bluetooth/osx/osxbtcentralmanager_p.h @@ -53,6 +53,7 @@ #include "qlowenergycontroller.h" #include "qlowenergyservice.h" +#include "osxbtgcdtimer_p.h" #include "qbluetoothuuid.h" #include "osxbtutility_p.h" #include "osxbluetooth_p.h" @@ -86,6 +87,18 @@ enum CentralManagerState CentralManagerDisconnecting }; +enum class OperationTimeout +{ + none, + serviceDiscovery, + includedServicesDiscovery, + characteristicsDiscovery, + characteristicRead, + descriptorsDiscovery, + descriptorRead, + characteristicWrite +}; + // In Qt we work with handles and UUIDs. Core Bluetooth // has NSArrays (and nested NSArrays inside servces/characteristics). // To simplify a navigation, I need a simple way to map from a handle @@ -132,7 +145,9 @@ typedef QHash<NSObject *, QByteArray> ValueHash; QT_END_NAMESPACE -@interface QT_MANGLE_NAMESPACE(OSXBTCentralManager) : NSObject<CBCentralManagerDelegate, CBPeripheralDelegate> +@interface QT_MANGLE_NAMESPACE(OSXBTCentralManager) : NSObject<CBCentralManagerDelegate, + CBPeripheralDelegate, + QT_MANGLE_NAMESPACE(GCDTimerDelegate)> { @private CBCentralManager *manager; @@ -166,6 +181,12 @@ QT_END_NAMESPACE QT_PREPEND_NAMESPACE(QLowEnergyHandle) currentReadHandle; QT_PREPEND_NAMESPACE(OSXBluetooth)::ValueHash valuesToWrite; + + qint64 timeoutMS; + id objectUnderWatch; + QT_PREPEND_NAMESPACE(OSXBluetooth)::OperationTimeout timeoutType; + QT_PREPEND_NAMESPACE(OSXBluetooth)::GCDTimer timeoutWatchdog; + @public CBPeripheral *peripheral; } diff --git a/src/bluetooth/osx/osxbtgcdtimer.mm b/src/bluetooth/osx/osxbtgcdtimer.mm new file mode 100644 index 00000000..095f8680 --- /dev/null +++ b/src/bluetooth/osx/osxbtgcdtimer.mm @@ -0,0 +1,111 @@ +/**************************************************************************** +** +** Copyright (C) 2018 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 "osxbtgcdtimer_p.h" +#include "osxbtutility_p.h" + +#include <QtCore/qdebug.h> + +#include <algorithm> + +@implementation QT_MANGLE_NAMESPACE(OSXBTGCDTimer) + +- (instancetype)initWithDelegate:(id<QT_MANGLE_NAMESPACE(GCDTimerDelegate)>)delegate +{ + if (self = [super init]) { + timeoutHandler = delegate; + timeoutMS = 0; + timeoutStepMS = 0; + cancelled = false; + } + return self; +} + +- (void)startWithTimeout:(qint64)ms step:(qint64)stepMS +{ + Q_ASSERT(!timeoutMS && !timeoutStepMS); + Q_ASSERT(!cancelled); + + if (!timeoutHandler) { + // Nobody to report timeout to, no need to start any task then. + return; + } + + if (ms <= 0 || stepMS <= 0) { + qCWarning(QT_BT_OSX, "Invalid timeout/step parameters"); + return; + } + + timeoutMS = ms; + timeoutStepMS = stepMS; + timer.start(); + + [self handleTimeout]; +} + +- (void)handleTimeout +{ + if (cancelled) + return; + + const qint64 elapsed = timer.elapsed(); + if (elapsed >= timeoutMS) { + [timeoutHandler timeout]; + } else { + using namespace QT_PREPEND_NAMESPACE(OSXBluetooth); + // Re-schedule: + dispatch_queue_t leQueue(qt_LE_queue()); + Q_ASSERT(leQueue); + const qint64 timeChunkMS = std::min(timeoutMS - elapsed, timeoutStepMS); + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, + int64_t(timeChunkMS / 1000. * NSEC_PER_SEC)), + leQueue, + ^{ + [self handleTimeout]; + }); + } +} + +- (void)cancelTimer +{ + cancelled = true; + timeoutHandler = nil; +} + +@end diff --git a/src/bluetooth/osx/osxbtgcdtimer_p.h b/src/bluetooth/osx/osxbtgcdtimer_p.h new file mode 100644 index 00000000..007a004b --- /dev/null +++ b/src/bluetooth/osx/osxbtgcdtimer_p.h @@ -0,0 +1,94 @@ +/**************************************************************************** +** +** Copyright (C) 2018 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$ +** +****************************************************************************/ + +#ifndef OSXBTGCDTIMER_P_H +#define OSXBTGCDTIMER_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "osxbtutility_p.h" + +#include <QtCore/qelapsedtimer.h> +#include <QtCore/qglobal.h> + +#include <Foundation/Foundation.h> + +@protocol QT_MANGLE_NAMESPACE(GCDTimerDelegate) +@required +- (void)timeout; +@end + +@interface QT_MANGLE_NAMESPACE(OSXBTGCDTimer) : NSObject { +@private + qint64 timeoutMS; + qint64 timeoutStepMS; + QT_PREPEND_NAMESPACE(QElapsedTimer) timer; + id<QT_MANGLE_NAMESPACE(GCDTimerDelegate)> timeoutHandler; + bool cancelled; +} + +- (instancetype)initWithDelegate:(id<QT_MANGLE_NAMESPACE(GCDTimerDelegate)>)delegate; +- (void)startWithTimeout:(qint64)ms step:(qint64)stepMS; +- (void)handleTimeout; +- (void)cancelTimer; + +@end + +QT_BEGIN_NAMESPACE + +namespace OSXBluetooth { + +using GCDTimerObjC = QT_MANGLE_NAMESPACE(OSXBTGCDTimer); +using GCDTimer = ObjCScopedPointer<GCDTimerObjC>; + +} + +QT_END_NAMESPACE + +#endif // OSXBTGCDTIMER_P_H + diff --git a/src/bluetooth/osx/osxbtledeviceinquiry.mm b/src/bluetooth/osx/osxbtledeviceinquiry.mm index 7a516dd4..2cece15b 100644 --- a/src/bluetooth/osx/osxbtledeviceinquiry.mm +++ b/src/bluetooth/osx/osxbtledeviceinquiry.mm @@ -65,9 +65,7 @@ QBluetoothUuid qt_uuid(NSUUID *nsUuid) } const int timeStepMS = 100; - const int powerOffTimeoutMS = 30000; -const qreal powerOffTimeStepS = 30. / 100.; struct AdvertisementData { // That's what CoreBluetooth has: @@ -115,14 +113,6 @@ QT_END_NAMESPACE QT_USE_NAMESPACE -@interface QT_MANGLE_NAMESPACE(OSXBTLEDeviceInquiry) (PrivateAPI) <CBCentralManagerDelegate> -// These two methods are scheduled with a small time step -// within a given timeout, they either re-schedule -// themselves or emit a signal/stop some operation. -- (void)stopScan; -- (void)handlePoweredOff; -@end - @implementation QT_MANGLE_NAMESPACE(OSXBTLEDeviceInquiry) -(id)initWithNotifier:(LECBManagerNotifier *)aNotifier @@ -153,60 +143,22 @@ QT_USE_NAMESPACE [super dealloc]; } -- (void)stopScan +- (void)timeout { - using namespace OSXBluetooth; - - // We never schedule stopScan if there is no timeout: - Q_ASSERT(inquiryTimeoutMS > 0); - if (internalState == InquiryActive) { - const int elapsed = scanTimer.elapsed(); - if (elapsed >= inquiryTimeoutMS) { - [manager stopScan]; - [manager setDelegate:nil]; - internalState = InquiryFinished; - Q_ASSERT(notifier); - emit notifier->discoveryFinished(); - } else { - // Re-schedule 'stopScan': - dispatch_queue_t leQueue(qt_LE_queue()); - Q_ASSERT(leQueue); - const int timeChunkMS = std::min(inquiryTimeoutMS - elapsed, timeStepMS); - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, - int64_t(timeChunkMS / 1000. * NSEC_PER_SEC)), - leQueue, - ^{ - [self stopScan]; - }); - } - } -} - -- (void)handlePoweredOff -{ - // 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. - using namespace OSXBluetooth; - - if (internalState == InquiryStarting) { - if (errorTimer.elapsed() >= powerOffTimeoutMS) { - [manager setDelegate:nil]; - internalState = ErrorPoweredOff; - Q_ASSERT(notifier); - emit notifier->CBManagerError(QBluetoothDeviceDiscoveryAgent::PoweredOffError); - } else { - dispatch_queue_t leQueue(qt_LE_queue()); - Q_ASSERT(leQueue); - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, - (int64_t)(powerOffTimeStepS * NSEC_PER_SEC)), - leQueue, - ^{ - [self handlePoweredOff]; - }); - } + [manager stopScan]; + [manager setDelegate:nil]; + internalState = InquiryFinished; + Q_ASSERT(notifier); + emit notifier->discoveryFinished(); + } else if (internalState == InquiryStarting) { + // 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 this as an error. + [manager setDelegate:nil]; + internalState = ErrorPoweredOff; + Q_ASSERT(notifier); + emit notifier->CBManagerError(QBluetoothDeviceDiscoveryAgent::PoweredOffError); } } @@ -220,6 +172,9 @@ QT_USE_NAMESPACE - (void)centralManagerDidUpdateState:(CBCentralManager *)central { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunguarded-availability-new" + if (central != manager) return; @@ -230,9 +185,6 @@ QT_USE_NAMESPACE using namespace OSXBluetooth; - dispatch_queue_t leQueue(qt_LE_queue()); - Q_ASSERT(leQueue); - const auto state = central.state; #if QT_IOS_PLATFORM_SDK_EQUAL_OR_ABOVE(__IPHONE_10_0) || QT_OSX_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_13) if (state == CBManagerStatePoweredOn) { @@ -243,18 +195,9 @@ QT_USE_NAMESPACE internalState = InquiryActive; if (inquiryTimeoutMS > 0) { - // We have a finite-length discovery, schedule stopScan, - // with a smaller time step, otherwise it can prevent - // 'self' from being deleted in time, which is not good - // (the block will retain 'self', waiting for timeout). - scanTimer.start(); - const int timeChunkMS = std::min(timeStepMS, inquiryTimeoutMS); - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, - int64_t(timeChunkMS / 1000. * NSEC_PER_SEC)), - leQueue, - ^{ - [self stopScan]; - }); + [elapsedTimer cancelTimer]; + elapsedTimer.reset([[GCDTimerObjC alloc] initWithDelegate:self]); + [elapsedTimer startWithTimeout:inquiryTimeoutMS step:timeStepMS]; } [manager scanForPeripheralsWithServices:nil options:nil]; @@ -284,19 +227,15 @@ QT_USE_NAMESPACE 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, + // enable Bluetooth in the "Settings" app. If a user does so, // we'll receive 'PoweredOn' state update later. - // No change in internalState. Wait for 30 seconds - // (we split it into smaller steps not to retain 'self' for - // too long ) ... - errorTimer.start(); - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, - (int64_t)(powerOffTimeStepS * NSEC_PER_SEC)), - leQueue, - ^{ - [self handlePoweredOff]; - }); + // No change in internalState. Wait for 30 seconds. + [elapsedTimer cancelTimer]; + elapsedTimer.reset([[GCDTimerObjC alloc] initWithDelegate:self]); + [elapsedTimer startWithTimeout:powerOffTimeoutMS step:300]; return; +#else + Q_UNUSED(powerOffTimeoutMS) #endif internalState = ErrorPoweredOff; emit notifier->CBManagerError(QBluetoothDeviceDiscoveryAgent::PoweredOffError); @@ -318,6 +257,8 @@ QT_USE_NAMESPACE // lost; an update is imminent. " // Wait for this imminent update. } + +#pragma clang diagnostic pop } - (void)stop @@ -325,6 +266,8 @@ QT_USE_NAMESPACE if (internalState == InquiryActive) [manager stopScan]; + [elapsedTimer cancelTimer]; + [manager setDelegate:nil]; internalState = InquiryCancelled; diff --git a/src/bluetooth/osx/osxbtledeviceinquiry_p.h b/src/bluetooth/osx/osxbtledeviceinquiry_p.h index fc787a6d..a19055ab 100644 --- a/src/bluetooth/osx/osxbtledeviceinquiry_p.h +++ b/src/bluetooth/osx/osxbtledeviceinquiry_p.h @@ -53,10 +53,10 @@ #include "qbluetoothdevicediscoveryagent.h" #include "qbluetoothdeviceinfo.h" +#include "osxbtgcdtimer_p.h" #include "osxbtutility_p.h" #include "osxbluetooth_p.h" -#include <QtCore/qelapsedtimer.h> #include <QtCore/qglobal.h> #include <QtCore/qlist.h> @@ -75,10 +75,8 @@ class LECBManagerNotifier; QT_END_NAMESPACE -// Ugly but all these QT_PREPEND_NAMESPACE etc. are even worse ... -using OSXBluetooth::LECBManagerNotifier; -using OSXBluetooth::ObjCScopedPointer; -using QT_PREPEND_NAMESPACE(QElapsedTimer); +using QT_PREPEND_NAMESPACE(OSXBluetooth)::LECBManagerNotifier; +using QT_PREPEND_NAMESPACE(OSXBluetooth)::ObjCScopedPointer; enum LEInquiryState { @@ -90,7 +88,7 @@ enum LEInquiryState ErrorLENotSupported }; -@interface QT_MANGLE_NAMESPACE(OSXBTLEDeviceInquiry) : NSObject +@interface QT_MANGLE_NAMESPACE(OSXBTLEDeviceInquiry) : NSObject<CBCentralManagerDelegate, QT_MANGLE_NAMESPACE(GCDTimerDelegate)> { LECBManagerNotifier *notifier; ObjCScopedPointer<CBCentralManager> manager; @@ -99,16 +97,14 @@ enum LEInquiryState LEInquiryState internalState; int inquiryTimeoutMS; - // Timers to check if we can execute delayed callbacks: - QT_PREPEND_NAMESPACE(QElapsedTimer) errorTimer; - QT_PREPEND_NAMESPACE(QElapsedTimer) scanTimer; + QT_PREPEND_NAMESPACE(OSXBluetooth)::GCDTimer elapsedTimer; } - (id)initWithNotifier:(LECBManagerNotifier *)aNotifier; - (void)dealloc; -// IMPORTANT: both 'startWithTimeout' and 'stop' -// can be executed only on the "Qt's LE queue". +// IMPORTANT: both 'startWithTimeout' and 'stop' MUST be executed on the "Qt's +// LE queue". - (void)startWithTimeout:(int)timeout; - (void)stop; diff --git a/src/bluetooth/osx/osxbtperipheralmanager.mm b/src/bluetooth/osx/osxbtperipheralmanager.mm index 64c8cd90..d3d92f41 100644 --- a/src/bluetooth/osx/osxbtperipheralmanager.mm +++ b/src/bluetooth/osx/osxbtperipheralmanager.mm @@ -388,6 +388,9 @@ bool qt_validate_value_range(const QLowEnergyCharacteristicData &data) - (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral { +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunguarded-availability-new" + if (peripheral != manager || !notifier) return; @@ -436,6 +439,8 @@ bool qt_validate_value_range(const QLowEnergyCharacteristicData &data) emit notifier->LEnotSupported(); state = PeripheralState::idle; } + +#pragma clang diagnostic pop } - (void)peripheralManager:(CBPeripheralManager *)peripheral |