| /**************************************************************************** |
| ** |
| ** 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 "qlowenergyserviceprivate_p.h" |
| #include "qlowenergycharacteristic.h" |
| #include "qlowenergycontroller.h" |
| #include "osxbtcentralmanager_p.h" |
| #include "osxbtnotifier_p.h" |
| |
| #include <QtCore/qloggingcategory.h> |
| #include <QtCore/qdebug.h> |
| |
| #include <algorithm> |
| #include <vector> |
| #include <limits> |
| |
| Q_DECLARE_METATYPE(QLowEnergyHandle) |
| |
| QT_BEGIN_NAMESPACE |
| |
| namespace OSXBluetooth { |
| |
| NSUInteger qt_countGATTEntries(CBService *service) |
| { |
| // Identify, how many characteristics/descriptors we have on a given service, |
| // +1 for the service itself. |
| // No checks if NSUInteger is big enough :) |
| // Let's assume such number of entries is not possible :) |
| |
| Q_ASSERT_X(service, Q_FUNC_INFO, "invalid service (nil)"); |
| |
| QT_BT_MAC_AUTORELEASEPOOL; |
| |
| NSArray *const cs = service.characteristics; |
| if (!cs || !cs.count) |
| return 1; |
| |
| NSUInteger n = 1 + cs.count; |
| for (CBCharacteristic *c in cs) { |
| NSArray *const ds = c.descriptors; |
| if (ds) |
| n += ds.count; |
| } |
| |
| 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*/); |
| } |
| |
| auto qt_find_watchdog(const std::vector<GCDTimer> &watchdogs, id object, OperationTimeout type) |
| { |
| return std::find_if(watchdogs.begin(), watchdogs.end(), [object, type](const GCDTimer &other){ |
| return [other objectUnderWatch] == object && [other timeoutType] == type;}); |
| } |
| |
| } // namespace OSXBluetooth |
| |
| QT_END_NAMESPACE |
| |
| QT_USE_NAMESPACE |
| |
| @interface QT_MANGLE_NAMESPACE(OSXBTCentralManager) (PrivateAPI) |
| |
| - (void)watchAfter:(id)object timeout:(OSXBluetooth::OperationTimeout)type; |
| - (bool)objectIsUnderWatch:(id)object operation:(OSXBluetooth::OperationTimeout)type; |
| - (void)stopWatchingAfter:(id)object operation:(OSXBluetooth::OperationTimeout)type; |
| - (void)stopWatchers; |
| - (void)retrievePeripheralAndConnect; |
| - (void)connectToPeripheral; |
| - (void)discoverIncludedServices; |
| - (void)readCharacteristics:(CBService *)service; |
| - (void)serviceDetailsDiscoveryFinished:(CBService *)service; |
| - (void)performNextRequest; |
| - (void)performNextReadRequest; |
| - (void)performNextWriteRequest; |
| |
| // Aux. functions. |
| - (CBService *)serviceForUUID:(const QBluetoothUuid &)qtUuid; |
| - (CBCharacteristic *)nextCharacteristicForService:(CBService*)service |
| startingFrom:(CBCharacteristic *)from; |
| - (CBCharacteristic *)nextCharacteristicForService:(CBService*)service |
| startingFrom:(CBCharacteristic *)from |
| withProperties:(CBCharacteristicProperties)properties; |
| - (CBDescriptor *)nextDescriptorForCharacteristic:(CBCharacteristic *)characteristic |
| startingFrom:(CBDescriptor *)descriptor; |
| - (CBDescriptor *)descriptor:(const QBluetoothUuid &)dUuid |
| forCharacteristic:(CBCharacteristic *)ch; |
| - (bool)cacheWriteValue:(const QByteArray &)value for:(NSObject *)obj; |
| - (void)handleReadWriteError:(NSError *)error; |
| - (void)reset; |
| |
| @end |
| |
| @implementation QT_MANGLE_NAMESPACE(OSXBTCentralManager) |
| { |
| @private |
| CBCentralManager *manager; |
| OSXBluetooth::CentralManagerState managerState; |
| bool disconnectPending; |
| |
| QBluetoothUuid deviceUuid; |
| |
| OSXBluetooth::LECBManagerNotifier *notifier; |
| |
| // Quite a verbose service discovery machinery |
| // (a "graph traversal"). |
| OSXBluetooth::ObjCStrongReference<NSMutableArray> servicesToVisit; |
| // The service we're discovering now (included services discovery): |
| NSUInteger currentService; |
| // Included services, we'll iterate through at the end of 'servicesToVisit': |
| OSXBluetooth::ObjCStrongReference<NSMutableArray> servicesToVisitNext; |
| // We'd like to avoid loops in a services' topology: |
| OSXBluetooth::ObjCStrongReference<NSMutableSet> visitedServices; |
| |
| QList<QBluetoothUuid> servicesToDiscoverDetails; |
| |
| OSXBluetooth::ServiceHash serviceMap; |
| OSXBluetooth::CharHash charMap; |
| OSXBluetooth::DescHash descMap; |
| |
| QLowEnergyHandle lastValidHandle; |
| |
| bool requestPending; |
| OSXBluetooth::RequestQueue requests; |
| QLowEnergyHandle currentReadHandle; |
| |
| OSXBluetooth::ValueHash valuesToWrite; |
| |
| qint64 timeoutMS; |
| std::vector<OSXBluetooth::GCDTimer> timeoutWatchdogs; |
| |
| CBPeripheral *peripheral; |
| } |
| |
| - (id)initWith:(OSXBluetooth::LECBManagerNotifier *)aNotifier |
| { |
| using namespace OSXBluetooth; |
| |
| if (self = [super init]) { |
| manager = nil; |
| managerState = CentralManagerIdle; |
| disconnectPending = false; |
| peripheral = nil; |
| notifier = aNotifier; |
| currentService = 0; |
| lastValidHandle = 0; |
| requestPending = false; |
| currentReadHandle = 0; |
| |
| 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; |
| } |
| |
| - (void)dealloc |
| { |
| // In the past I had a 'transient delegate': I've seen some crashes |
| // while deleting a manager _before_ its state updated. |
| // Strangely enough, I can not reproduce this anymore, so this |
| // part is simplified now. To be investigated though. |
| |
| visitedServices.reset(nil); |
| servicesToVisit.reset(nil); |
| servicesToVisitNext.reset(nil); |
| |
| [manager setDelegate:nil]; |
| [manager release]; |
| |
| [peripheral setDelegate:nil]; |
| [peripheral release]; |
| |
| if (notifier) |
| notifier->deleteLater(); |
| |
| [self stopWatchers]; |
| [super dealloc]; |
| } |
| |
| - (CBPeripheral *)peripheral |
| { |
| return peripheral; |
| } |
| |
| - (void)watchAfter:(id)object timeout:(OSXBluetooth::OperationTimeout)type |
| { |
| using namespace OSXBluetooth; |
| |
| GCDTimer newWatcher([[GCDTimerObjC alloc] initWithDelegate:self], false /*do not retain*/); |
| [newWatcher watchAfter:object withTimeoutType:type]; |
| timeoutWatchdogs.push_back(newWatcher); |
| [newWatcher startWithTimeout:timeoutMS step:200]; |
| } |
| |
| - (bool)objectIsUnderWatch:(id)object operation:(OSXBluetooth::OperationTimeout)type |
| { |
| return OSXBluetooth::qt_find_watchdog(timeoutWatchdogs, object, type) != timeoutWatchdogs.end(); |
| } |
| |
| - (void)stopWatchingAfter:(id)object operation:(OSXBluetooth::OperationTimeout)type |
| { |
| auto pos = OSXBluetooth::qt_find_watchdog(timeoutWatchdogs, object, type); |
| if (pos != timeoutWatchdogs.end()) { |
| [*pos cancelTimer]; |
| timeoutWatchdogs.erase(pos); |
| } |
| } |
| |
| - (void)stopWatchers |
| { |
| for (auto &watchdog : timeoutWatchdogs) |
| [watchdog cancelTimer]; |
| timeoutWatchdogs.clear(); |
| } |
| |
| - (void)timeout:(id)sender |
| { |
| Q_UNUSED(sender) |
| |
| using namespace OSXBluetooth; |
| |
| GCDTimerObjC *watcher = static_cast<GCDTimerObjC *>(sender); |
| id cbObject = [watcher objectUnderWatch]; |
| const OperationTimeout type = [watcher timeoutType]; |
| |
| Q_ASSERT([self objectIsUnderWatch:cbObject operation:type]); |
| |
| NSLog(@"Timeout caused by: %@", cbObject); |
| |
| // Note that after this switch the 'watcher' is released (we don't |
| // own it anymore), though GCD is probably still holding a reference. |
| const ObjCStrongReference<NSError> nsError(qt_timeoutNSError(type)); |
| switch (type) { |
| 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:cbObject error:nsError]; |
| break; |
| case OperationTimeout::characteristicsDiscovery: |
| qCWarning(QT_BT_OSX, "Timeout in characteristics discovery"); |
| [self peripheral:peripheral didDiscoverCharacteristicsForService:cbObject error:nsError]; |
| break; |
| case OperationTimeout::characteristicRead: |
| qCWarning(QT_BT_OSX, "Timeout while reading a characteristic"); |
| [self peripheral:peripheral didUpdateValueForCharacteristic:cbObject error:nsError]; |
| break; |
| case OperationTimeout::descriptorsDiscovery: |
| qCWarning(QT_BT_OSX, "Timeout in descriptors discovery"); |
| [self peripheral:peripheral didDiscoverDescriptorsForCharacteristic:cbObject error:nsError]; |
| break; |
| case OperationTimeout::descriptorRead: |
| qCWarning(QT_BT_OSX, "Timeout while reading a descriptor"); |
| [self peripheral:peripheral didUpdateValueForDescriptor:cbObject error:nsError]; |
| break; |
| case OperationTimeout::characteristicWrite: |
| qCWarning(QT_BT_OSX, "Timeout while writing a characteristic with response"); |
| [self peripheral:peripheral didWriteValueForCharacteristic:cbObject error:nsError]; |
| default:; |
| } |
| } |
| |
| - (void)connectToDevice:(const QBluetoothUuid &)aDeviceUuid |
| { |
| disconnectPending = false; // Cancel the previous disconnect if any. |
| deviceUuid = aDeviceUuid; |
| |
| if (!manager) { |
| // The first time we try to connect, no manager created yet, |
| // no status update received. |
| if (const dispatch_queue_t leQueue = OSXBluetooth::qt_LE_queue()) { |
| managerState = OSXBluetooth::CentralManagerUpdating; |
| manager = [[CBCentralManager alloc] initWithDelegate:self queue:leQueue]; |
| } |
| |
| if (!manager) { |
| managerState = OSXBluetooth::CentralManagerIdle; |
| qCWarning(QT_BT_OSX) << "failed to allocate a central manager"; |
| if (notifier) |
| emit notifier->CBManagerError(QLowEnergyController::ConnectionError); |
| } |
| } else if (managerState != OSXBluetooth::CentralManagerUpdating) { |
| [self retrievePeripheralAndConnect]; |
| } |
| } |
| |
| - (void)retrievePeripheralAndConnect |
| { |
| Q_ASSERT_X(manager, Q_FUNC_INFO, "invalid central manager (nil)"); |
| Q_ASSERT_X(managerState == OSXBluetooth::CentralManagerIdle, |
| Q_FUNC_INFO, "invalid state"); |
| |
| if ([self isConnected]) { |
| qCDebug(QT_BT_OSX) << "already connected"; |
| if (notifier) |
| emit notifier->connected(); |
| return; |
| } else if (peripheral) { |
| // Was retrieved already, but not connected |
| // or disconnected. |
| [self connectToPeripheral]; |
| return; |
| } |
| |
| using namespace OSXBluetooth; |
| |
| // Retrieve a peripheral first ... |
| ObjCScopedPointer<NSMutableArray> uuids([[NSMutableArray alloc] init]); |
| if (!uuids) { |
| qCWarning(QT_BT_OSX) << "failed to allocate identifiers"; |
| if (notifier) |
| emit notifier->CBManagerError(QLowEnergyController::ConnectionError); |
| return; |
| } |
| |
| |
| const quint128 qtUuidData(deviceUuid.toUInt128()); |
| uuid_t uuidData = {}; |
| std::copy(qtUuidData.data, qtUuidData.data + 16, uuidData); |
| const ObjCScopedPointer<NSUUID> nsUuid([[NSUUID alloc] initWithUUIDBytes:uuidData]); |
| if (!nsUuid) { |
| qCWarning(QT_BT_OSX) << "failed to allocate NSUUID identifier"; |
| if (notifier) |
| emit notifier->CBManagerError(QLowEnergyController::ConnectionError); |
| return; |
| } |
| |
| [uuids addObject:nsUuid]; |
| // With the latest CoreBluetooth, we can synchronously retrive peripherals: |
| QT_BT_MAC_AUTORELEASEPOOL; |
| NSArray *const peripherals = [manager retrievePeripheralsWithIdentifiers:uuids]; |
| if (!peripherals || peripherals.count != 1) { |
| qCWarning(QT_BT_OSX) << "failed to retrive a peripheral"; |
| if (notifier) |
| emit notifier->CBManagerError(QLowEnergyController::UnknownRemoteDeviceError); |
| return; |
| } |
| |
| peripheral = [static_cast<CBPeripheral *>([peripherals objectAtIndex:0]) retain]; |
| [self connectToPeripheral]; |
| } |
| |
| - (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 == CentralManagerIdle, Q_FUNC_INFO, "invalid state"); |
| |
| // The state is still the same - connecting. |
| if ([self isConnected]) { |
| qCDebug(QT_BT_OSX) << "already connected"; |
| if (notifier) |
| emit notifier->connected(); |
| } else { |
| qCDebug(QT_BT_OSX) << "trying to connect"; |
| managerState = CentralManagerConnecting; |
| [manager connectPeripheral:peripheral options:nil]; |
| } |
| } |
| |
| - (bool)isConnected |
| { |
| if (!peripheral) |
| return false; |
| |
| return peripheral.state == CBPeripheralStateConnected; |
| } |
| |
| - (void)disconnectFromDevice |
| { |
| [self reset]; |
| |
| if (managerState == OSXBluetooth::CentralManagerUpdating) { |
| disconnectPending = true; // this is for 'didUpdate' method. |
| if (notifier) { |
| // We were waiting for the first update |
| // with 'PoweredOn' status, when suddenly got disconnected called. |
| // Since we have not attempted to connect yet, emit now. |
| // Note: we do not change the state, since we still maybe interested |
| // in the status update before the next connect attempt. |
| emit notifier->disconnected(); |
| } |
| } else { |
| disconnectPending = false; |
| if ([self isConnected]) |
| managerState = OSXBluetooth::CentralManagerDisconnecting; |
| else |
| managerState = OSXBluetooth::CentralManagerIdle; |
| |
| // We have to call -cancelPeripheralConnection: even |
| // if not connected (to cancel a pending connect attempt). |
| // Unfortunately, didDisconnect callback is not always called |
| // (despite of Apple's docs saying it _must_ be). |
| if (peripheral) |
| [manager cancelPeripheralConnection:peripheral]; |
| } |
| } |
| |
| - (void)discoverServices |
| { |
| using namespace OSXBluetooth; |
| |
| Q_ASSERT_X(peripheral, Q_FUNC_INFO, "invalid peripheral (nil)"); |
| Q_ASSERT_X(managerState == CentralManagerIdle, Q_FUNC_INFO, "invalid state"); |
| |
| // From Apple's docs: |
| // |
| //"If the servicesUUIDs parameter is nil, all the available |
| //services of the peripheral are returned; setting the |
| //parameter to nil is considerably slower and is not recommended." |
| // |
| // ... but we'd like to have them all: |
| [peripheral setDelegate:self]; |
| managerState = CentralManagerDiscovering; |
| [self watchAfter:peripheral timeout:OperationTimeout::serviceDiscovery]; |
| [peripheral discoverServices:nil]; |
| } |
| |
| - (void)discoverIncludedServices |
| { |
| using namespace OSXBluetooth; |
| |
| Q_ASSERT_X(managerState == CentralManagerIdle, Q_FUNC_INFO, "invalid state"); |
| Q_ASSERT_X(manager, Q_FUNC_INFO, "invalid manager (nil)"); |
| Q_ASSERT_X(peripheral, Q_FUNC_INFO, "invalid peripheral (nil)"); |
| |
| QT_BT_MAC_AUTORELEASEPOOL; |
| |
| NSArray *const services = peripheral.services; |
| if (!services || !services.count) { |
| // A peripheral without any services at all. |
| if (notifier) |
| emit notifier->serviceDiscoveryFinished(); |
| } else { |
| // 'reset' also calls retain on a parameter. |
| servicesToVisitNext.reset(nil); |
| servicesToVisit.reset([NSMutableArray arrayWithArray:services]); |
| currentService = 0; |
| visitedServices.reset([NSMutableSet setWithCapacity:peripheral.services.count]); |
| |
| CBService *const s = [services objectAtIndex:currentService]; |
| [visitedServices addObject:s]; |
| managerState = CentralManagerDiscovering; |
| [self watchAfter:s timeout:OperationTimeout::includedServicesDiscovery]; |
| [peripheral discoverIncludedServices:nil forService:s]; |
| } |
| } |
| |
| - (void)discoverServiceDetails:(const QBluetoothUuid &)serviceUuid |
| { |
| // This function does not change 'managerState', since it |
| // can be called concurrently (not waiting for the previous |
| // discovery to finish). |
| |
| using namespace OSXBluetooth; |
| |
| Q_ASSERT_X(managerState != CentralManagerUpdating, Q_FUNC_INFO, "invalid state"); |
| Q_ASSERT_X(!serviceUuid.isNull(), Q_FUNC_INFO, "invalid service UUID"); |
| Q_ASSERT_X(peripheral, Q_FUNC_INFO, "invalid peripheral (nil)"); |
| |
| if (servicesToDiscoverDetails.contains(serviceUuid)) { |
| qCWarning(QT_BT_OSX) << "already discovering for" |
| << serviceUuid; |
| return; |
| } |
| |
| QT_BT_MAC_AUTORELEASEPOOL; |
| |
| if (CBService *const service = [self serviceForUUID:serviceUuid]) { |
| servicesToDiscoverDetails.append(serviceUuid); |
| [self watchAfter:service timeout:OperationTimeout::characteristicsDiscovery]; |
| [peripheral discoverCharacteristics:nil forService:service]; |
| return; |
| } |
| |
| qCWarning(QT_BT_OSX) << "unknown service uuid" |
| << serviceUuid; |
| |
| if (notifier) |
| emit notifier->CBManagerError(serviceUuid, QLowEnergyService::UnknownError); |
| } |
| |
| - (void)readCharacteristics:(CBService *)service |
| { |
| // This method does not change 'managerState', we can |
| // have several 'detail discoveries' active. |
| Q_ASSERT_X(service, Q_FUNC_INFO, "invalid service (nil)"); |
| |
| using namespace OSXBluetooth; |
| |
| QT_BT_MAC_AUTORELEASEPOOL; |
| |
| Q_ASSERT_X(managerState != CentralManagerUpdating, Q_FUNC_INFO, "invalid state"); |
| Q_ASSERT_X(manager, Q_FUNC_INFO, "invalid manager (nil)"); |
| Q_ASSERT_X(peripheral, Q_FUNC_INFO, "invalid peripheral (nil)"); |
| |
| if (!service.characteristics || !service.characteristics.count) |
| return [self serviceDetailsDiscoveryFinished:service]; |
| |
| NSArray *const cs = service.characteristics; |
| for (CBCharacteristic *c in cs) { |
| if (c.properties & CBCharacteristicPropertyRead) { |
| [self watchAfter:c timeout:OperationTimeout::characteristicRead]; |
| return [peripheral readValueForCharacteristic:c]; |
| } |
| } |
| |
| // No readable properties? Discover descriptors then: |
| [self discoverDescriptors:service]; |
| } |
| |
| - (void)discoverDescriptors:(CBService *)service |
| { |
| // This method does not change 'managerState', we can have |
| // several discoveries active. |
| Q_ASSERT_X(service, Q_FUNC_INFO, "invalid service (nil)"); |
| |
| using namespace OSXBluetooth; |
| |
| QT_BT_MAC_AUTORELEASEPOOL; |
| |
| Q_ASSERT_X(managerState != CentralManagerUpdating, |
| Q_FUNC_INFO, "invalid state"); |
| Q_ASSERT_X(manager, Q_FUNC_INFO, "invalid manager (nil)"); |
| Q_ASSERT_X(peripheral, Q_FUNC_INFO, "invalid peripheral (nil)"); |
| |
| if (!service.characteristics || !service.characteristics.count) { |
| [self serviceDetailsDiscoveryFinished:service]; |
| } else { |
| // Start from 0 and continue in the callback. |
| 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 != CentralManagerUpdating, Q_FUNC_INFO, "invalid state"); |
| Q_ASSERT_X(peripheral, Q_FUNC_INFO, "invalid peripheral (nil)"); |
| |
| QT_BT_MAC_AUTORELEASEPOOL; |
| |
| NSArray *const cs = service.characteristics; |
| // 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) { |
| CBDescriptor *desc = [c.descriptors objectAtIndex:0]; |
| [self watchAfter:desc timeout:OperationTimeout::descriptorRead]; |
| return [peripheral readValueForDescriptor:desc]; |
| } |
| } |
| |
| // No descriptors to read, done. |
| [self serviceDetailsDiscoveryFinished:service]; |
| } |
| |
| - (void)serviceDetailsDiscoveryFinished:(CBService *)service |
| { |
| Q_ASSERT_X(service, Q_FUNC_INFO, "invalid service (nil)"); |
| |
| using namespace OSXBluetooth; |
| |
| QT_BT_MAC_AUTORELEASEPOOL; |
| |
| const QBluetoothUuid serviceUuid(qt_uuid(service.UUID)); |
| servicesToDiscoverDetails.removeAll(serviceUuid); |
| |
| const NSUInteger nHandles = qt_countGATTEntries(service); |
| Q_ASSERT_X(nHandles, Q_FUNC_INFO, "unexpected number of GATT entires"); |
| |
| const QLowEnergyHandle maxHandle = std::numeric_limits<QLowEnergyHandle>::max(); |
| if (nHandles >= maxHandle || lastValidHandle > maxHandle - nHandles) { |
| // Well, that's unlikely :) But we must be sure. |
| qCWarning(QT_BT_OSX) << "can not allocate more handles"; |
| if (notifier) |
| notifier->CBManagerError(serviceUuid, QLowEnergyService::OperationError); |
| return; |
| } |
| |
| // A temporary service object to pass the details. |
| // Set only uuid, characteristics and descriptors (and probably values), |
| // nothing else is needed. |
| QSharedPointer<QLowEnergyServicePrivate> qtService(new QLowEnergyServicePrivate); |
| qtService->uuid = serviceUuid; |
| // We 'register' handles/'CBentities' even if qlowenergycontroller (delegate) |
| // later fails to do this with some error. Otherwise, if we try to implement |
| // rollback/transaction logic interface is getting too ugly/complicated. |
| ++lastValidHandle; |
| serviceMap[lastValidHandle] = service; |
| qtService->startHandle = lastValidHandle; |
| |
| NSArray *const cs = service.characteristics; |
| // Now map chars/descriptors and handles. |
| if (cs && cs.count) { |
| QHash<QLowEnergyHandle, QLowEnergyServicePrivate::CharData> charList; |
| |
| for (CBCharacteristic *c in cs) { |
| ++lastValidHandle; |
| // Register this characteristic: |
| charMap[lastValidHandle] = c; |
| // Create a Qt's internal characteristic: |
| QLowEnergyServicePrivate::CharData newChar = {}; |
| newChar.uuid = qt_uuid(c.UUID); |
| const int cbProps = c.properties & 0xff; |
| newChar.properties = static_cast<QLowEnergyCharacteristic::PropertyTypes>(cbProps); |
| newChar.value = qt_bytearray(c.value); |
| newChar.valueHandle = lastValidHandle; |
| |
| NSArray *const ds = c.descriptors; |
| if (ds && ds.count) { |
| QHash<QLowEnergyHandle, QLowEnergyServicePrivate::DescData> descList; |
| for (CBDescriptor *d in ds) { |
| // Register this descriptor: |
| ++lastValidHandle; |
| descMap[lastValidHandle] = d; |
| // Create a Qt's internal descriptor: |
| QLowEnergyServicePrivate::DescData newDesc = {}; |
| newDesc.uuid = qt_uuid(d.UUID); |
| newDesc.value = qt_bytearray(static_cast<NSObject *>(d.value)); |
| descList[lastValidHandle] = newDesc; |
| // Check, if it's client characteristic configuration descriptor: |
| if (newDesc.uuid == QBluetoothUuid::ClientCharacteristicConfiguration) { |
| if (newDesc.value.size() && (newDesc.value[0] & 3)) |
| [peripheral setNotifyValue:YES forCharacteristic:c]; |
| } |
| } |
| |
| newChar.descriptorList = descList; |
| } |
| |
| charList[newChar.valueHandle] = newChar; |
| } |
| |
| qtService->characteristicList = charList; |
| } |
| |
| qtService->endHandle = lastValidHandle; |
| |
| if (notifier) |
| emit notifier->serviceDetailsDiscoveryFinished(qtService); |
| } |
| |
| - (void)performNextRequest |
| { |
| using namespace OSXBluetooth; |
| |
| if (requestPending || !requests.size()) |
| return; |
| |
| switch (requests.head().type) { |
| case LERequest::CharRead: |
| case LERequest::DescRead: |
| return [self performNextReadRequest]; |
| case LERequest::CharWrite: |
| case LERequest::DescWrite: |
| case LERequest::ClientConfiguration: |
| return [self performNextWriteRequest]; |
| default: |
| // Should never happen. |
| Q_ASSERT(0); |
| } |
| } |
| |
| - (void)performNextReadRequest |
| { |
| using namespace OSXBluetooth; |
| |
| Q_ASSERT_X(peripheral, Q_FUNC_INFO, "invalid peripheral (nil)"); |
| Q_ASSERT_X(!requestPending, Q_FUNC_INFO, "processing another request"); |
| Q_ASSERT_X(requests.size(), Q_FUNC_INFO, "no requests to handle"); |
| Q_ASSERT_X(requests.head().type == LERequest::CharRead |
| || requests.head().type == LERequest::DescRead, |
| Q_FUNC_INFO, "not a read request"); |
| |
| const LERequest request(requests.dequeue()); |
| if (request.type == LERequest::CharRead) { |
| if (!charMap.contains(request.handle)) { |
| qCWarning(QT_BT_OSX) << "characteristic with handle" |
| << request.handle << "not found"; |
| return [self performNextRequest]; |
| } |
| |
| 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)) { |
| qCWarning(QT_BT_OSX) << "descriptor with handle" |
| << request.handle << "not found"; |
| return [self performNextRequest]; |
| } |
| |
| requestPending = true; |
| currentReadHandle = request.handle; |
| // Timeouts: see the comment above (CharRead). |
| [peripheral readValueForDescriptor:descMap[request.handle]]; |
| } |
| } |
| |
| - (void)performNextWriteRequest |
| { |
| using namespace OSXBluetooth; |
| |
| Q_ASSERT_X(peripheral, Q_FUNC_INFO, "invalid peripheral (nil)"); |
| Q_ASSERT_X(!requestPending, Q_FUNC_INFO, "processing another request"); |
| Q_ASSERT_X(requests.size(), Q_FUNC_INFO, "no requests to handle"); |
| Q_ASSERT_X(requests.head().type == LERequest::CharWrite |
| || requests.head().type == LERequest::DescWrite |
| || requests.head().type == LERequest::ClientConfiguration, |
| Q_FUNC_INFO, "not a write request"); |
| |
| const LERequest request(requests.dequeue()); |
| |
| if (request.type == LERequest::DescWrite) { |
| if (!descMap.contains(request.handle)) { |
| qCWarning(QT_BT_OSX) << "handle:" << request.handle |
| << "not found"; |
| return [self performNextRequest]; |
| } |
| |
| CBDescriptor *const descriptor = descMap[request.handle]; |
| ObjCStrongReference<NSData> data(data_from_bytearray(request.value)); |
| if (!data) { |
| // Even if qtData.size() == 0, we still need NSData object. |
| qCWarning(QT_BT_OSX) << "failed to allocate an NSData object"; |
| return [self performNextRequest]; |
| } |
| |
| if (![self cacheWriteValue:request.value for:descriptor]) |
| return [self performNextRequest]; |
| |
| requestPending = true; |
| return [peripheral writeValue:data.data() forDescriptor:descriptor]; |
| } else { |
| if (!charMap.contains(request.handle)) { |
| qCWarning(QT_BT_OSX) << "characteristic with handle:" |
| << request.handle << "not found"; |
| return [self performNextRequest]; |
| } |
| |
| CBCharacteristic *const characteristic = charMap[request.handle]; |
| |
| if (request.type == LERequest::ClientConfiguration) { |
| const QBluetoothUuid qtUuid(QBluetoothUuid::ClientCharacteristicConfiguration); |
| CBDescriptor *const descriptor = [self descriptor:qtUuid forCharacteristic:characteristic]; |
| Q_ASSERT_X(descriptor, Q_FUNC_INFO, "no client characteristic " |
| "configuration descriptor found"); |
| |
| if (![self cacheWriteValue:request.value for:descriptor]) |
| return [self performNextRequest]; |
| |
| bool enable = false; |
| if (request.value.size()) |
| enable = request.value[0] & 3; |
| |
| requestPending = true; |
| [peripheral setNotifyValue:enable forCharacteristic:characteristic]; |
| } else { |
| ObjCStrongReference<NSData> data(data_from_bytearray(request.value)); |
| if (!data) { |
| // Even if qtData.size() == 0, we still need NSData object. |
| qCWarning(QT_BT_OSX) << "failed to allocate NSData object"; |
| return [self performNextRequest]; |
| } |
| |
| // TODO: check what happens if I'm using NSData with length 0. |
| if (request.withResponse) { |
| if (![self cacheWriteValue:request.value for:characteristic]) |
| return [self performNextRequest]; |
| |
| requestPending = true; |
| [self watchAfter:characteristic timeout:OperationTimeout::characteristicWrite]; |
| [peripheral writeValue:data.data() forCharacteristic:characteristic |
| type:CBCharacteristicWriteWithResponse]; |
| } else { |
| [peripheral writeValue:data.data() forCharacteristic:characteristic |
| type:CBCharacteristicWriteWithoutResponse]; |
| [self performNextRequest]; |
| } |
| } |
| } |
| } |
| |
| - (void)setNotifyValue:(const QByteArray &)value |
| forCharacteristic:(QLowEnergyHandle)charHandle |
| onService:(const QBluetoothUuid &)serviceUuid |
| { |
| using namespace OSXBluetooth; |
| |
| Q_ASSERT_X(charHandle, Q_FUNC_INFO, "invalid characteristic handle (0)"); |
| |
| if (!charMap.contains(charHandle)) { |
| qCWarning(QT_BT_OSX) << "unknown characteristic handle" |
| << charHandle; |
| if (notifier) { |
| emit notifier->CBManagerError(serviceUuid, |
| QLowEnergyService::DescriptorWriteError); |
| } |
| return; |
| } |
| |
| // At the moment we call setNotifyValue _only_ from 'writeDescriptor'; |
| // from Qt's API POV it's a descriptor write operation and we must report |
| // it back, so check _now_ that we really have this descriptor. |
| const QBluetoothUuid qtUuid(QBluetoothUuid::ClientCharacteristicConfiguration); |
| if (![self descriptor:qtUuid forCharacteristic:charMap[charHandle]]) { |
| qCWarning(QT_BT_OSX) << "no client characteristic configuration found"; |
| if (notifier) { |
| emit notifier->CBManagerError(serviceUuid, |
| QLowEnergyService::DescriptorWriteError); |
| } |
| return; |
| } |
| |
| LERequest request; |
| request.type = LERequest::ClientConfiguration; |
| request.handle = charHandle; |
| request.value = value; |
| |
| requests.enqueue(request); |
| [self performNextRequest]; |
| } |
| |
| - (void)readCharacteristic:(QLowEnergyHandle)charHandle |
| onService:(const QBluetoothUuid &)serviceUuid |
| { |
| using namespace OSXBluetooth; |
| |
| Q_ASSERT_X(charHandle, Q_FUNC_INFO, "invalid characteristic handle (0)"); |
| |
| QT_BT_MAC_AUTORELEASEPOOL; |
| |
| if (!charMap.contains(charHandle)) { |
| qCWarning(QT_BT_OSX) << "characteristic:" << charHandle << "not found"; |
| if (notifier) { |
| emit notifier->CBManagerError(serviceUuid, |
| QLowEnergyService::CharacteristicReadError); |
| |
| } |
| return; |
| } |
| |
| LERequest request; |
| request.type = LERequest::CharRead; |
| request.handle = charHandle; |
| |
| requests.enqueue(request); |
| [self performNextRequest]; |
| } |
| |
| - (void)write:(const QByteArray &)value |
| charHandle:(QLowEnergyHandle)charHandle |
| onService:(const QBluetoothUuid &)serviceUuid |
| withResponse:(bool)withResponse |
| { |
| using namespace OSXBluetooth; |
| |
| Q_ASSERT_X(charHandle, Q_FUNC_INFO, "invalid characteristic handle (0)"); |
| |
| QT_BT_MAC_AUTORELEASEPOOL; |
| |
| if (!charMap.contains(charHandle)) { |
| qCWarning(QT_BT_OSX) << "characteristic:" << charHandle << "not found"; |
| if (notifier) { |
| emit notifier->CBManagerError(serviceUuid, |
| QLowEnergyService::CharacteristicWriteError); |
| } |
| return; |
| } |
| |
| LERequest request; |
| request.type = LERequest::CharWrite; |
| request.withResponse = withResponse; |
| request.handle = charHandle; |
| request.value = value; |
| |
| requests.enqueue(request); |
| [self performNextRequest]; |
| } |
| |
| - (void)readDescriptor:(QLowEnergyHandle)descHandle |
| onService:(const QBluetoothUuid &)serviceUuid |
| { |
| using namespace OSXBluetooth; |
| |
| Q_ASSERT_X(descHandle, Q_FUNC_INFO, "invalid descriptor handle (0)"); |
| |
| if (!descMap.contains(descHandle)) { |
| qCWarning(QT_BT_OSX) << "handle:" << descHandle << "not found"; |
| if (notifier) { |
| emit notifier->CBManagerError(serviceUuid, |
| QLowEnergyService::DescriptorReadError); |
| } |
| return; |
| } |
| |
| LERequest request; |
| request.type = LERequest::DescRead; |
| request.handle = descHandle; |
| |
| requests.enqueue(request); |
| [self performNextRequest]; |
| } |
| |
| - (void)write:(const QByteArray &)value |
| descHandle:(QLowEnergyHandle)descHandle |
| onService:(const QBluetoothUuid &)serviceUuid |
| { |
| using namespace OSXBluetooth; |
| |
| Q_ASSERT_X(descHandle, Q_FUNC_INFO, "invalid descriptor handle (0)"); |
| |
| if (!descMap.contains(descHandle)) { |
| qCWarning(QT_BT_OSX) << "handle:" << descHandle << "not found"; |
| if (notifier) { |
| emit notifier->CBManagerError(serviceUuid, |
| QLowEnergyService::DescriptorWriteError); |
| } |
| return; |
| } |
| |
| LERequest request; |
| request.type = LERequest::DescWrite; |
| request.handle = descHandle; |
| request.value = value; |
| |
| requests.enqueue(request); |
| [self performNextRequest]; |
| } |
| |
| // Aux. methods: |
| |
| - (CBService *)serviceForUUID:(const QBluetoothUuid &)qtUuid |
| { |
| using namespace OSXBluetooth; |
| |
| Q_ASSERT_X(!qtUuid.isNull(), Q_FUNC_INFO, "invalid uuid"); |
| Q_ASSERT_X(peripheral, Q_FUNC_INFO, "invalid peripheral (nil)"); |
| |
| ObjCStrongReference<NSMutableArray> toVisit([NSMutableArray arrayWithArray:peripheral.services], true); |
| ObjCStrongReference<NSMutableArray> toVisitNext([[NSMutableArray alloc] init], false); |
| ObjCStrongReference<NSMutableSet> visitedNodes([[NSMutableSet alloc] init], false); |
| |
| while (true) { |
| for (NSUInteger i = 0, e = [toVisit count]; i < e; ++i) { |
| CBService *const s = [toVisit objectAtIndex:i]; |
| if (equal_uuids(s.UUID, qtUuid)) |
| return s; |
| if (![visitedNodes containsObject:s] && s.includedServices && s.includedServices.count) { |
| [visitedNodes addObject:s]; |
| [toVisitNext addObjectsFromArray:s.includedServices]; |
| } |
| } |
| |
| if (![toVisitNext count]) |
| return nil; |
| |
| toVisit.resetWithoutRetain(toVisitNext.take()); |
| toVisitNext.resetWithoutRetain([[NSMutableArray alloc] init]); |
| } |
| |
| return nil; |
| } |
| |
| - (CBCharacteristic *)nextCharacteristicForService:(CBService*)service |
| startingFrom:(CBCharacteristic *)characteristic |
| { |
| Q_ASSERT_X(service, Q_FUNC_INFO, "invalid service (nil)"); |
| Q_ASSERT_X(characteristic, Q_FUNC_INFO, "invalid characteristic (nil)"); |
| Q_ASSERT_X(service.characteristics, Q_FUNC_INFO, "invalid service"); |
| Q_ASSERT_X(service.characteristics.count, Q_FUNC_INFO, "invalid service"); |
| |
| QT_BT_MAC_AUTORELEASEPOOL; |
| |
| // TODO: test that we NEVER have the same characteristic twice in array! |
| // At the moment I just protect against this by iterating in a reverse |
| // order (at least avoiding a potential inifite loop with '-indexOfObject:'). |
| NSArray *const cs = service.characteristics; |
| if (cs.count == 1) |
| return nil; |
| |
| for (NSUInteger index = cs.count - 1; index != 0; --index) { |
| if ([cs objectAtIndex:index] == characteristic) { |
| if (index + 1 == cs.count) |
| return nil; |
| else |
| return [cs objectAtIndex:index + 1]; |
| } |
| } |
| |
| Q_ASSERT_X([cs objectAtIndex:0] == characteristic, Q_FUNC_INFO, |
| "characteristic was not found in service.characteristics"); |
| |
| return [cs objectAtIndex:1]; |
| } |
| |
| - (CBCharacteristic *)nextCharacteristicForService:(CBService*)service |
| startingFrom:(CBCharacteristic *)characteristic |
| properties:(CBCharacteristicProperties)properties |
| { |
| Q_ASSERT_X(service, Q_FUNC_INFO, "invalid service (nil)"); |
| Q_ASSERT_X(characteristic, Q_FUNC_INFO, "invalid characteristic (nil)"); |
| Q_ASSERT_X(service.characteristics, Q_FUNC_INFO, "invalid service"); |
| Q_ASSERT_X(service.characteristics.count, Q_FUNC_INFO, "invalid service"); |
| |
| QT_BT_MAC_AUTORELEASEPOOL; |
| |
| // TODO: test that we NEVER have the same characteristic twice in array! |
| // At the moment I just protect against this by iterating in a reverse |
| // order (at least avoiding a potential inifite loop with '-indexOfObject:'). |
| NSArray *const cs = service.characteristics; |
| if (cs.count == 1) |
| return nil; |
| |
| NSUInteger index = cs.count - 1; |
| for (; index != 0; --index) { |
| if ([cs objectAtIndex:index] == characteristic) { |
| if (index + 1 == cs.count) { |
| return nil; |
| } else { |
| index += 1; |
| break; |
| } |
| } |
| } |
| |
| if (!index) { |
| Q_ASSERT_X([cs objectAtIndex:0] == characteristic, Q_FUNC_INFO, |
| "characteristic not found in service.characteristics"); |
| index = 1; |
| } |
| |
| for (const NSUInteger e = cs.count; index < e; ++index) { |
| CBCharacteristic *const c = [cs objectAtIndex:index]; |
| if (c.properties & properties) |
| return c; |
| } |
| |
| return nil; |
| } |
| |
| - (CBDescriptor *)nextDescriptorForCharacteristic:(CBCharacteristic *)characteristic |
| startingFrom:(CBDescriptor *)descriptor |
| { |
| Q_ASSERT_X(characteristic, Q_FUNC_INFO, "invalid characteristic (nil)"); |
| Q_ASSERT_X(descriptor, Q_FUNC_INFO, "invalid descriptor (nil)"); |
| Q_ASSERT_X(characteristic.descriptors, Q_FUNC_INFO, "invalid characteristic"); |
| Q_ASSERT_X(characteristic.descriptors.count, Q_FUNC_INFO, "invalid characteristic"); |
| |
| QT_BT_MAC_AUTORELEASEPOOL; |
| |
| NSArray *const ds = characteristic.descriptors; |
| if (ds.count == 1) |
| return nil; |
| |
| for (NSUInteger index = ds.count - 1; index != 0; --index) { |
| if ([ds objectAtIndex:index] == descriptor) { |
| if (index + 1 == ds.count) |
| return nil; |
| else |
| return [ds objectAtIndex:index + 1]; |
| } |
| } |
| |
| Q_ASSERT_X([ds objectAtIndex:0] == descriptor, Q_FUNC_INFO, |
| "descriptor was not found in characteristic.descriptors"); |
| |
| return [ds objectAtIndex:1]; |
| } |
| |
| - (CBDescriptor *)descriptor:(const QBluetoothUuid &)qtUuid |
| forCharacteristic:(CBCharacteristic *)ch |
| { |
| if (qtUuid.isNull() || !ch) |
| return nil; |
| |
| QT_BT_MAC_AUTORELEASEPOOL; |
| |
| CBDescriptor *descriptor = nil; |
| NSArray *const ds = ch.descriptors; |
| if (ds && ds.count) { |
| for (CBDescriptor *d in ds) { |
| if (OSXBluetooth::equal_uuids(d.UUID, qtUuid)) { |
| descriptor = d; |
| break; |
| } |
| } |
| } |
| |
| return descriptor; |
| } |
| |
| - (bool)cacheWriteValue:(const QByteArray &)value for:(NSObject *)obj |
| { |
| Q_ASSERT_X(obj, Q_FUNC_INFO, "invalid object (nil)"); |
| |
| if ([obj isKindOfClass:[CBCharacteristic class]]) { |
| CBCharacteristic *const ch = static_cast<CBCharacteristic *>(obj); |
| if (!charMap.key(ch)) { |
| qCWarning(QT_BT_OSX) << "unexpected characteristic, no handle found"; |
| return false; |
| } |
| } else if ([obj isKindOfClass:[CBDescriptor class]]) { |
| CBDescriptor *const d = static_cast<CBDescriptor *>(obj); |
| if (!descMap.key(d)) { |
| qCWarning(QT_BT_OSX) << "unexpected descriptor, no handle found"; |
| return false; |
| } |
| } else { |
| qCWarning(QT_BT_OSX) << "invalid object, characteristic " |
| "or descriptor required"; |
| return false; |
| } |
| |
| if (valuesToWrite.contains(obj)) { |
| // It can be a result of some previous errors - for example, |
| // we never got a callback from a previous write. |
| qCWarning(QT_BT_OSX) << "already has a cached value for this " |
| "object, the value will be replaced"; |
| } |
| |
| valuesToWrite[obj] = value; |
| return true; |
| } |
| |
| - (void)reset |
| { |
| requestPending = false; |
| valuesToWrite.clear(); |
| requests.clear(); |
| servicesToDiscoverDetails.clear(); |
| lastValidHandle = 0; |
| serviceMap.clear(); |
| charMap.clear(); |
| descMap.clear(); |
| currentReadHandle = 0; |
| [self stopWatchers]; |
| // TODO: also serviceToVisit/VisitNext and visitedServices ? |
| } |
| |
| - (void)handleReadWriteError:(NSError *)error |
| { |
| Q_ASSERT(notifier); |
| |
| switch (error.code) { |
| case 0x05: // GATT_INSUFFICIENT_AUTHORIZATION |
| case 0x0F: // GATT_INSUFFICIENT_ENCRYPTION |
| emit notifier->CBManagerError(QLowEnergyController::AuthorizationError); |
| [self detach]; |
| break; |
| default: |
| break; |
| } |
| } |
| |
| // CBCentralManagerDelegate (the real one). |
| |
| - (void)centralManagerDidUpdateState:(CBCentralManager *)central |
| { |
| 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 |
| || state == CBManagerStateResetting) { |
| #else |
| if (state == CBCentralManagerStateUnknown |
| || state == CBCentralManagerStateResetting) { |
| #endif |
| // We still have to wait, docs say: |
| // "The current state of the central manager is unknown; |
| // an update is imminent." or |
| // "The connection with the system service was momentarily |
| // lost; an update is imminent." |
| return; |
| } |
| |
| // Let's check some states we do not like first: |
| #if QT_IOS_PLATFORM_SDK_EQUAL_OR_ABOVE(__IPHONE_10_0) || QT_OSX_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_13) |
| if (state == CBManagerStateUnsupported || state == CBManagerStateUnauthorized) { |
| #else |
| if (state == CBCentralManagerStateUnsupported || state == CBCentralManagerStateUnauthorized) { |
| #endif |
| if (managerState == CentralManagerUpdating) { |
| // We tried to connect just to realize, LE is not supported. Report this. |
| managerState = CentralManagerIdle; |
| if (notifier) |
| emit notifier->LEnotSupported(); |
| } else { |
| // TODO: if we are here, LE _was_ supported and we first managed to update |
| // and reset managerState from CentralManagerUpdating. |
| managerState = CentralManagerIdle; |
| if (notifier) |
| emit notifier->CBManagerError(QLowEnergyController::InvalidBluetoothAdapterError); |
| } |
| [self stopWatchers]; |
| return; |
| } |
| |
| #if QT_IOS_PLATFORM_SDK_EQUAL_OR_ABOVE(__IPHONE_10_0) || QT_OSX_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_13) |
| if (state == CBManagerStatePoweredOff) { |
| #else |
| if (state == CBCentralManagerStatePoweredOff) { |
| #endif |
| |
| 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) |
| emit notifier->CBManagerError(QLowEnergyController::InvalidBluetoothAdapterError); |
| } |
| [self stopWatchers]; |
| return; |
| } |
| |
| #if QT_IOS_PLATFORM_SDK_EQUAL_OR_ABOVE(__IPHONE_10_0) || QT_OSX_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_13) |
| if (state == CBManagerStatePoweredOn) { |
| #else |
| if (state == CBCentralManagerStatePoweredOn) { |
| #endif |
| if (managerState == CentralManagerUpdating && !disconnectPending) { |
| managerState = CentralManagerIdle; |
| [self retrievePeripheralAndConnect]; |
| } |
| } else { |
| // We actually handled all known states, but .. Core Bluetooth can change? |
| Q_ASSERT_X(0, Q_FUNC_INFO, "invalid central's state"); |
| } |
| |
| #pragma clang diagnostic pop |
| } |
| |
| - (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)aPeripheral |
| { |
| Q_UNUSED(central) |
| Q_UNUSED(aPeripheral) |
| |
| if (managerState != OSXBluetooth::CentralManagerConnecting) { |
| // We called cancel but before disconnected, managed to connect? |
| return; |
| } |
| |
| managerState = OSXBluetooth::CentralManagerIdle; |
| if (notifier) |
| emit notifier->connected(); |
| } |
| |
| - (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)aPeripheral |
| error:(NSError *)error |
| { |
| Q_UNUSED(central) |
| Q_UNUSED(aPeripheral) |
| Q_UNUSED(error) |
| |
| if (managerState != OSXBluetooth::CentralManagerConnecting) { |
| // Canceled already. |
| return; |
| } |
| |
| managerState = OSXBluetooth::CentralManagerIdle; |
| // TODO: better error mapping is required. |
| if (notifier) |
| notifier->CBManagerError(QLowEnergyController::UnknownRemoteDeviceError); |
| } |
| |
| - (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)aPeripheral |
| error:(NSError *)error |
| { |
| Q_UNUSED(central) |
| Q_UNUSED(aPeripheral) |
| |
| // Clear internal caches/data. |
| [self reset]; |
| |
| if (error && managerState == OSXBluetooth::CentralManagerDisconnecting) { |
| managerState = OSXBluetooth::CentralManagerIdle; |
| qCWarning(QT_BT_OSX) << "failed to disconnect"; |
| if (notifier) |
| emit notifier->CBManagerError(QLowEnergyController::UnknownRemoteDeviceError); |
| } else { |
| managerState = OSXBluetooth::CentralManagerIdle; |
| if (notifier) |
| emit notifier->disconnected(); |
| } |
| } |
| |
| // CBPeripheralDelegate. |
| |
| - (void)peripheral:(CBPeripheral *)aPeripheral didDiscoverServices:(NSError *)error |
| { |
| Q_UNUSED(aPeripheral) |
| |
| if (managerState != OSXBluetooth::CentralManagerDiscovering) { |
| // Canceled by -disconnectFromDevice, or as a result of a timeout. |
| return; |
| } |
| |
| using namespace OSXBluetooth; |
| if (![self objectIsUnderWatch:aPeripheral operation:OperationTimeout::serviceDiscovery]) // Timed out already |
| return; |
| |
| QT_BT_MAC_AUTORELEASEPOOL; |
| |
| [self stopWatchingAfter:aPeripheral operation:OperationTimeout::serviceDiscovery]; |
| |
| managerState = OSXBluetooth::CentralManagerIdle; |
| |
| if (error) { |
| NSLog(@"%s failed with error %@", Q_FUNC_INFO, error); |
| // TODO: better error mapping required. |
| if (notifier) |
| emit notifier->CBManagerError(QLowEnergyController::UnknownError); |
| } |
| |
| [self discoverIncludedServices]; |
| } |
| |
| - (void)peripheral:(CBPeripheral *)aPeripheral |
| didModifyServices:(NSArray<CBService *> *)invalidatedServices |
| { |
| Q_UNUSED(aPeripheral) |
| Q_UNUSED(invalidatedServices) |
| |
| qCWarning(QT_BT_OSX) << "The peripheral has modified its services."; |
| // "This method is invoked whenever one or more services of a peripheral have changed. |
| // A peripheral’s services have changed if: |
| // * A service is removed from the peripheral’s database |
| // * A new service is added to the peripheral’s database |
| // * A service that was previously removed from the peripheral’s |
| // database is readded to the database at a different location" |
| |
| // In case new services were added - we have to discover them. |
| // In case some were removed - we can end up with dangling pointers |
| // (see our 'watchdogs', for example). To handle the situation |
| // we stop all current operations here, report to QLowEnergyController |
| // so that it can trigger re-discovery. |
| [self reset]; |
| managerState = OSXBluetooth::CentralManagerIdle; |
| if (notifier) |
| emit notifier->servicesWereModified(); |
| } |
| |
| - (void)peripheral:(CBPeripheral *)aPeripheral didDiscoverIncludedServicesForService:(CBService *)service |
| error:(NSError *)error |
| { |
| Q_UNUSED(aPeripheral) |
| |
| using namespace OSXBluetooth; |
| |
| if (managerState != CentralManagerDiscovering) { |
| // Canceled by disconnectFromDevice or -peripheralDidDisconnect... |
| return; |
| } |
| |
| if (![self objectIsUnderWatch:service operation:OperationTimeout::includedServicesDiscovery]) |
| return; |
| |
| QT_BT_MAC_AUTORELEASEPOOL; |
| |
| Q_ASSERT_X(peripheral, Q_FUNC_INFO, "invalid peripheral (nil)"); |
| |
| [self stopWatchingAfter:service operation:OperationTimeout::includedServicesDiscovery]; |
| managerState = CentralManagerIdle; |
| |
| if (error) { |
| NSLog(@"%s: finished with error %@ for service %@", |
| Q_FUNC_INFO, error, service.UUID); |
| } else if (service.includedServices && service.includedServices.count) { |
| // Now we have even more services to do included services discovery ... |
| if (!servicesToVisitNext) |
| servicesToVisitNext.reset([NSMutableArray arrayWithArray:service.includedServices]); |
| else |
| [servicesToVisitNext addObjectsFromArray:service.includedServices]; |
| } |
| |
| // Do we have something else to discover on this 'level'? |
| ++currentService; |
| |
| for (const NSUInteger e = [servicesToVisit count]; currentService < e; ++currentService) { |
| CBService *const s = [servicesToVisit objectAtIndex:currentService]; |
| if (![visitedServices containsObject:s]) { |
| // Continue with discovery ... |
| [visitedServices addObject:s]; |
| managerState = CentralManagerDiscovering; |
| [self watchAfter:s timeout:OperationTimeout::includedServicesDiscovery]; |
| return [peripheral discoverIncludedServices:nil forService:s]; |
| } |
| } |
| |
| // No services to visit more on this 'level'. |
| |
| if (servicesToVisitNext && [servicesToVisitNext count]) { |
| servicesToVisit.resetWithoutRetain(servicesToVisitNext.take()); |
| |
| currentService = 0; |
| for (const NSUInteger e = [servicesToVisit count]; currentService < e; ++currentService) { |
| CBService *const s = [servicesToVisit objectAtIndex:currentService]; |
| if (![visitedServices containsObject:s]) { |
| [visitedServices addObject:s]; |
| managerState = CentralManagerDiscovering; |
| [self watchAfter:s timeout:OperationTimeout::includedServicesDiscovery]; |
| return [peripheral discoverIncludedServices:nil forService:s]; |
| } |
| } |
| } |
| |
| // Finally, if we're here, the service discovery is done! |
| |
| // Release all these things now, no need to prolong their lifetime. |
| visitedServices.reset(nil); |
| servicesToVisit.reset(nil); |
| servicesToVisitNext.reset(nil); |
| |
| if (notifier) |
| emit notifier->serviceDiscoveryFinished(); |
| } |
| |
| - (void)peripheral:(CBPeripheral *)aPeripheral didDiscoverCharacteristicsForService:(CBService *)service |
| error:(NSError *)error |
| { |
| // This method does not change 'managerState', we can have several |
| // discoveries active. |
| Q_UNUSED(aPeripheral) |
| |
| if (!notifier) { |
| // Detached. |
| return; |
| } |
| |
| using namespace OSXBluetooth; |
| |
| if (![self objectIsUnderWatch:service operation:OperationTimeout::characteristicsDiscovery]) |
| return; |
| |
| QT_BT_MAC_AUTORELEASEPOOL; |
| |
| [self stopWatchingAfter:service operation:OperationTimeout::characteristicsDiscovery]; |
| |
| Q_ASSERT_X(managerState != CentralManagerUpdating, Q_FUNC_INFO, "invalid state"); |
| |
| if (error) { |
| NSLog(@"%s failed with error: %@", Q_FUNC_INFO, error); |
| // 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); |
| } |
| |
| [self readCharacteristics:service]; |
| } |
| |
| - (void)peripheral:(CBPeripheral *)aPeripheral |
| didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic |
| error:(NSError *)error |
| { |
| Q_UNUSED(aPeripheral) |
| |
| if (!notifier) // Detached. |
| return; |
| |
| using namespace OSXBluetooth; |
| |
| QT_BT_MAC_AUTORELEASEPOOL; |
| |
| const bool readMatch = [self objectIsUnderWatch:characteristic operation:OperationTimeout::characteristicRead]; |
| if (readMatch) |
| [self stopWatchingAfter:characteristic operation:OperationTimeout::characteristicRead]; |
| |
| Q_ASSERT_X(managerState != CentralManagerUpdating, Q_FUNC_INFO, "invalid state"); |
| Q_ASSERT_X(peripheral, Q_FUNC_INFO, "invalid peripheral (nil)"); |
| |
| |
| // First, let's check if we're discovering a service details now. |
| CBService *const service = characteristic.service; |
| const QBluetoothUuid qtUuid(qt_uuid(service.UUID)); |
| const bool isDetailsDiscovery = servicesToDiscoverDetails.contains(qtUuid); |
| const QLowEnergyHandle chHandle = charMap.key(characteristic); |
| |
| if (error) { |
| NSLog(@"%s failed with error %@", Q_FUNC_INFO, error); |
| if (!isDetailsDiscovery) { |
| if (chHandle && chHandle == currentReadHandle) { |
| currentReadHandle = 0; |
| requestPending = false; |
| emit notifier->CBManagerError(qtUuid, QLowEnergyService::CharacteristicReadError); |
| [self handleReadWriteError:error]; |
| [self performNextRequest]; |
| } |
| return; |
| } |
| } |
| |
| if (isDetailsDiscovery) { |
| 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) - |
| // if something esle is wrong (we subscribed for a notification), |
| // disconnected (but other application is connected) and still receiveing |
| // updated values ... |
| // TODO: this must be properly tested. |
| if (!chHandle) { |
| qCCritical(QT_BT_OSX) << "unexpected update notification, " |
| "no characteristic handle found"; |
| return; |
| } |
| |
| if (currentReadHandle == chHandle) { |
| // Even if it was not a reply to our read request (no way to test it) |
| // report it. |
| requestPending = false; |
| currentReadHandle = 0; |
| // |
| emit notifier->characteristicRead(chHandle, qt_bytearray(characteristic.value)); |
| [self performNextRequest]; |
| } else { |
| emit notifier->characteristicUpdated(chHandle, qt_bytearray(characteristic.value)); |
| } |
| } |
| } |
| |
| - (void)peripheral:(CBPeripheral *)aPeripheral |
| didDiscoverDescriptorsForCharacteristic:(CBCharacteristic *)characteristic |
| error:(NSError *)error |
| { |
| // This method does not change 'managerState', we can |
| // have several discoveries active at the same time. |
| Q_UNUSED(aPeripheral) |
| |
| if (!notifier) { |
| // Detached, no need to continue ... |
| return; |
| } |
| |
| QT_BT_MAC_AUTORELEASEPOOL; |
| |
| using namespace OSXBluetooth; |
| |
| if (![self objectIsUnderWatch:characteristic operation:OperationTimeout::descriptorsDiscovery]) |
| return; |
| |
| [self stopWatchingAfter:characteristic operation:OperationTimeout::descriptorsDiscovery]; |
| |
| if (error) { |
| NSLog(@"%s failed with error %@", Q_FUNC_INFO, error); |
| // We can continue though ... |
| } |
| |
| // Do we have more characteristics on this service to discover descriptors? |
| CBCharacteristic *const next = [self nextCharacteristicForService:characteristic.service |
| startingFrom:characteristic]; |
| if (next) { |
| [self watchAfter:next timeout:OperationTimeout::descriptorsDiscovery]; |
| [peripheral discoverDescriptorsForCharacteristic:next]; |
| } else { |
| [self readDescriptors:characteristic.service]; |
| } |
| } |
| |
| - (void)peripheral:(CBPeripheral *)aPeripheral |
| didUpdateValueForDescriptor:(CBDescriptor *)descriptor |
| error:(NSError *)error |
| { |
| Q_UNUSED(aPeripheral) |
| |
| Q_ASSERT_X(peripheral, Q_FUNC_INFO, "invalid peripheral (nil)"); |
| |
| if (!notifier) { |
| // Detached ... |
| return; |
| } |
| |
| QT_BT_MAC_AUTORELEASEPOOL; |
| |
| using namespace OSXBluetooth; |
| |
| if (![self objectIsUnderWatch:descriptor operation:OperationTimeout::descriptorRead]) |
| return; |
| |
| [self stopWatchingAfter:descriptor operation:OperationTimeout::descriptorRead]; |
| |
| CBService *const service = descriptor.characteristic.service; |
| const QBluetoothUuid qtUuid(qt_uuid(service.UUID)); |
| const bool isDetailsDiscovery = servicesToDiscoverDetails.contains(qtUuid); |
| const QLowEnergyHandle dHandle = descMap.key(descriptor); |
| |
| if (error) { |
| NSLog(@"%s failed with error %@", Q_FUNC_INFO, error); |
| |
| if (!isDetailsDiscovery) { |
| if (dHandle && dHandle == currentReadHandle) { |
| currentReadHandle = 0; |
| requestPending = false; |
| emit notifier->CBManagerError(qtUuid, QLowEnergyService::DescriptorReadError); |
| [self handleReadWriteError:error]; |
| [self performNextRequest]; |
| } |
| return; |
| } |
| } |
| |
| if (isDetailsDiscovery) { |
| // Test if we have any other characteristic to read yet. |
| 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 |
| // on a given characteristic, or continue with the |
| // next characteristic in a given service (if any). |
| CBCharacteristic *const ch = descriptor.characteristic; |
| CBCharacteristic *nextCh = [self nextCharacteristicForService:ch.service |
| startingFrom:ch]; |
| while (nextCh) { |
| 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]; |
| } |
| |
| [self serviceDetailsDiscoveryFinished:service]; |
| } |
| } else { |
| if (!dHandle) { |
| qCCritical(QT_BT_OSX) << "unexpected value update notification, " |
| "no descriptor handle found"; |
| return; |
| } |
| |
| if (dHandle == currentReadHandle) { |
| currentReadHandle = 0; |
| requestPending = false; |
| emit notifier->descriptorRead(dHandle, qt_bytearray(static_cast<NSObject *>(descriptor.value))); |
| [self performNextRequest]; |
| } |
| } |
| } |
| |
| - (void)peripheral:(CBPeripheral *)aPeripheral |
| didWriteValueForCharacteristic:(CBCharacteristic *)characteristic |
| error:(NSError *)error |
| { |
| Q_UNUSED(aPeripheral) |
| Q_UNUSED(characteristic) |
| |
| if (!notifier) { |
| // Detached. |
| return; |
| } |
| |
| // From docs: |
| // |
| // "This method is invoked only when your app calls the writeValue:forCharacteristic:type: |
| // method with the CBCharacteristicWriteWithResponse constant specified as the write type. |
| // If successful, the error parameter is nil. If unsuccessful, |
| // the error parameter returns the cause of the failure." |
| |
| using namespace OSXBluetooth; |
| |
| QT_BT_MAC_AUTORELEASEPOOL; |
| |
| if (![self objectIsUnderWatch:characteristic operation:OperationTimeout::characteristicWrite]) |
| return; |
| |
| [self stopWatchingAfter:characteristic operation:OperationTimeout::characteristicWrite]; |
| requestPending = false; |
| |
| // Error or not, but the cached value has to be deleted ... |
| const QByteArray valueToReport(valuesToWrite.value(characteristic, QByteArray())); |
| if (!valuesToWrite.remove(characteristic)) { |
| qCWarning(QT_BT_OSX) << "no updated value found " |
| "for characteristic"; |
| } |
| |
| if (error) { |
| NSLog(@"%s failed with error %@", Q_FUNC_INFO, error); |
| emit notifier->CBManagerError(qt_uuid(characteristic.service.UUID), |
| QLowEnergyService::CharacteristicWriteError); |
| [self handleReadWriteError:error]; |
| } else { |
| const QLowEnergyHandle cHandle = charMap.key(characteristic); |
| emit notifier->characteristicWritten(cHandle, valueToReport); |
| } |
| |
| [self performNextRequest]; |
| } |
| |
| - (void)peripheral:(CBPeripheral *)aPeripheral |
| didWriteValueForDescriptor:(CBDescriptor *)descriptor |
| error:(NSError *)error |
| { |
| Q_UNUSED(aPeripheral) |
| |
| if (!notifier) { |
| // Detached already. |
| return; |
| } |
| |
| using namespace OSXBluetooth; |
| |
| QT_BT_MAC_AUTORELEASEPOOL; |
| |
| requestPending = false; |
| |
| // Error or not, a value (if any) must be removed. |
| const QByteArray valueToReport(valuesToWrite.value(descriptor, QByteArray())); |
| if (!valuesToWrite.remove(descriptor)) |
| qCWarning(QT_BT_OSX) << "no updated value found"; |
| |
| if (error) { |
| NSLog(@"%s failed with error %@", Q_FUNC_INFO, error); |
| emit notifier->CBManagerError(qt_uuid(descriptor.characteristic.service.UUID), |
| QLowEnergyService::DescriptorWriteError); |
| [self handleReadWriteError:error]; |
| } else { |
| const QLowEnergyHandle dHandle = descMap.key(descriptor); |
| Q_ASSERT_X(dHandle, Q_FUNC_INFO, "descriptor not found in the descriptors map"); |
| emit notifier->descriptorWritten(dHandle, valueToReport); |
| } |
| |
| [self performNextRequest]; |
| } |
| |
| - (void)peripheral:(CBPeripheral *)aPeripheral |
| didUpdateNotificationStateForCharacteristic:(CBCharacteristic *)characteristic |
| error:(NSError *)error |
| { |
| Q_UNUSED(aPeripheral) |
| |
| if (!notifier) |
| return; |
| |
| using namespace OSXBluetooth; |
| |
| QT_BT_MAC_AUTORELEASEPOOL; |
| |
| requestPending = false; |
| |
| const QBluetoothUuid qtUuid(QBluetoothUuid::ClientCharacteristicConfiguration); |
| CBDescriptor *const descriptor = [self descriptor:qtUuid forCharacteristic:characteristic]; |
| const QByteArray valueToReport(valuesToWrite.value(descriptor, QByteArray())); |
| const int nRemoved = valuesToWrite.remove(descriptor); |
| |
| if (error) { |
| NSLog(@"%s failed with error %@", Q_FUNC_INFO, error); |
| // In Qt's API it's a descriptor write actually. |
| emit notifier->CBManagerError(qt_uuid(characteristic.service.UUID), |
| QLowEnergyService::DescriptorWriteError); |
| } else if (nRemoved) { |
| const QLowEnergyHandle dHandle = descMap.key(descriptor); |
| emit notifier->descriptorWritten(dHandle, valueToReport); |
| } |
| |
| [self performNextRequest]; |
| } |
| |
| - (void)detach |
| { |
| if (notifier) { |
| notifier->disconnect(); |
| notifier->deleteLater(); |
| notifier = nullptr; |
| } |
| |
| [self stopWatchers]; |
| [self disconnectFromDevice]; |
| } |
| |
| @end |