diff options
author | Timur Pocheptsov <Timur.Pocheptsov@digia.com> | 2014-11-07 14:00:40 +0100 |
---|---|---|
committer | Timur Pocheptsov <Timur.Pocheptsov@digia.com> | 2014-11-12 14:32:35 +0100 |
commit | 51dbe4a457c0bf81b96c898aab5d83b421e7a4f0 (patch) | |
tree | cc1c878a608c0d03ea7c67367548cc7b73322aa8 /src/bluetooth | |
parent | be5883540fe61123da3fb39dd39cfd34eec442b8 (diff) |
QLowEnergyController - connectTo/disconnectFrom/Device (OS X, iOS)
Implement connect to device/disconnect from device methods,
add CentralManager class dealing with CBCentralManager (with
additional external logic).
- Improve error handling while in 'connecting' state.
- Use the proper error (ConnectionError) instead of UnknownError +
make the error handling more uniform while isConnecting == true.
- Use isNull on QBluetoothUuid, no need in ugly 'hasUuid'.
Change-Id: I84a704a746b4677ccb870b9c132db5f7359030c6
Reviewed-by: Alex Blasche <alexander.blasche@theqtcompany.com>
Diffstat (limited to 'src/bluetooth')
-rw-r--r-- | src/bluetooth/osx/osxbt.pri | 6 | ||||
-rw-r--r-- | src/bluetooth/osx/osxbtcentralmanager.mm | 448 | ||||
-rw-r--r-- | src/bluetooth/osx/osxbtcentralmanager_p.h | 128 | ||||
-rw-r--r-- | src/bluetooth/qlowenergycontroller_osx.mm | 179 | ||||
-rw-r--r-- | src/bluetooth/qlowenergycontroller_osx_p.h | 27 |
5 files changed, 780 insertions, 8 deletions
diff --git a/src/bluetooth/osx/osxbt.pri b/src/bluetooth/osx/osxbt.pri index 288afa6d..1eddcf4c 100644 --- a/src/bluetooth/osx/osxbt.pri +++ b/src/bluetooth/osx/osxbt.pri @@ -12,7 +12,8 @@ CONFIG(osx) { osx/osxbtobexsession_p.h \ osx/osxbtledeviceinquiry_p.h \ osx/corebluetoothwrapper_p.h \ - osx/osxbtcentralmanagerdelegate_p.h + osx/osxbtcentralmanagerdelegate_p.h \ + osx/osxbtcentralmanager_p.h OBJECTIVE_SOURCES += osx/osxbtutility.mm \ osx/osxbtdevicepair.mm \ @@ -26,7 +27,8 @@ CONFIG(osx) { osx/osxbtsocketlistener.mm \ osx/osxbtobexsession.mm \ osx/osxbtledeviceinquiry.mm \ - osx/osxbtcentralmanagerdelegate.mm + osx/osxbtcentralmanagerdelegate.mm \ + osx/osxbtcentralmanager.mm } else { PRIVATE_HEADERS += osx/osxbtutility_p.h \ osx/osxbtledeviceinquiry_p.h \ diff --git a/src/bluetooth/osx/osxbtcentralmanager.mm b/src/bluetooth/osx/osxbtcentralmanager.mm new file mode 100644 index 00000000..d2e45832 --- /dev/null +++ b/src/bluetooth/osx/osxbtcentralmanager.mm @@ -0,0 +1,448 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** 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 Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/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 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "osxbtcentralmanagerdelegate_p.h" +#include "osxbtcentralmanager_p.h" + +#include <QtCore/qloggingcategory.h> +#include <QtCore/qdebug.h> + +#include <algorithm> + +QT_BEGIN_NAMESPACE + +namespace OSXBluetooth { + +CentralManagerDelegate::~CentralManagerDelegate() +{ +} + +} + +QT_END_NAMESPACE + + +#ifdef QT_NAMESPACE +using namespace QT_NAMESPACE; +#endif + +@interface QT_MANGLE_NAMESPACE(OSXBTCentralManager) (PrivateAPI) + +- (QLowEnergyController::Error)connectToDevice; // "Device" is in Qt's world ... +- (void)connectToPeripheral; // "Peripheral" is in Core Bluetooth. +- (bool)connected; + +@end + +@implementation QT_MANGLE_NAMESPACE(OSXBTCentralManager) + +- (id)initWithDelegate:(QT_PREPEND_NAMESPACE(OSXBluetooth)::CentralManagerDelegate *)aDelegate +{ + Q_ASSERT_X(aDelegate, "-initWithDelegate:", "invalid delegate (null)"); + + if (self = [super init]) { + manager = nil; + managerState = OSXBluetooth::CentralManagerIdle; + disconnectPending = false; + peripheral = nil; + delegate = aDelegate; + } + + return self; +} + +- (void)dealloc +{ + typedef QT_MANGLE_NAMESPACE(OSXBTCentralManagerTransientDelegate) TransientDelegate; + + if (managerState == OSXBluetooth::CentralManagerUpdating) { + // Here we have to trick with a transient delegate not to + // delete the manager too early - or Core Bluetooth will crash. + // State was not updated yet, too early to release. + TransientDelegate *const transient = [[TransientDelegate alloc] initWithManager:manager]; + // On ARC the lifetime of a transient delegate will become a problem, since delegate itself + // is a weak reference in a manager. + [manager setDelegate:transient]; + } else { + [manager setDelegate:nil]; + [manager release]; + } + + [peripheral setDelegate:nil]; + [peripheral release]; + + [super dealloc]; +} + +- (QLowEnergyController::Error)connectToDevice:(const QBluetoothUuid &)aDeviceUuid +{ + Q_ASSERT_X(managerState == OSXBluetooth::CentralManagerIdle, "-connectToDevice", + "invalid state"); // QLowEnergyController connects only if in UnconnectedState. + + deviceUuid = aDeviceUuid; + + if (!manager) { + managerState = OSXBluetooth::CentralManagerUpdating; // We'll have to wait for updated state. + manager = [[CBCentralManager alloc] initWithDelegate:self queue:nil]; + if (!manager) { + managerState = OSXBluetooth::CentralManagerIdle; + qCWarning(QT_BT_OSX) << "-connectToDevice:, failed to allocate a " + "central manager"; + return QLowEnergyController::ConnectionError; + } + return QLowEnergyController::NoError; + } else { + return [self connectToDevice]; + } +} + +- (QLowEnergyController::Error)connectToDevice +{ + Q_ASSERT_X(manager, "-connectToDevice", "invalid central manager (nil)"); + Q_ASSERT_X(managerState == OSXBluetooth::CentralManagerIdle, + "-connectToDevice", "invalid state"); + + if ([self isConnected]) { + qCDebug(QT_BT_OSX) << "-connectToDevice, already connected"; + delegate->connectSuccess(); + return QLowEnergyController::NoError; + } else if (peripheral) { + // Was retrieved already, but not connected + // or disconnected. + [self connectToPeripheral]; + return QLowEnergyController::NoError; + } + + using namespace OSXBluetooth; + + // Retrieve a peripheral first ... + ObjCScopedPointer<NSMutableArray> uuids([[NSMutableArray alloc] init]); + if (!uuids) { + qCWarning(QT_BT_OSX) << "-connectToDevice, failed to allocate identifiers"; + return QLowEnergyController::ConnectionError; + } + +#if QT_MAC_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_9, __IPHONE_6_0) + const quint128 qtUuidData(deviceUuid.toUInt128()); + // STATIC_ASSERT on sizes would be handy! + 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) << "-connectToDevice, failed to allocate NSUUID identifier"; + return QLowEnergyController::ConnectionError; + } + + [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) << "-connectToDevice, failed to retrive a peripheral"; + return QLowEnergyController::UnknownRemoteDeviceError; + } + + peripheral = [static_cast<CBPeripheral *>([peripherals objectAtIndex:0]) retain]; + [self connectToPeripheral]; + + return QLowEnergyController::NoError; +#else + OSXBluetooth::CFStrongReference<CFUUIDRef> cfUuid(OSXBluetooth::cf_uuid(deviceUuid)); + if (!cfUuid) { + qCWarning(QT_BT_OSX) << "-connectToDevice:, failed to create CFUUID object"; + return QLowEnergyController::ConnectionError; + } + // TODO: With ARC this cast will be illegal: + [uuids addObject:(id)cfUuid.data()]; + // Unfortunately, with old Core Bluetooth this call is asynchronous ... + managerState = OSXBluetooth::CentralManagerConnecting; + [manager retrievePeripherals:uuids]; + + return QLowEnergyController::NoError; +#endif +} + +- (void)connectToPeripheral +{ + Q_ASSERT_X(manager, "-connectToPeripheral", "invalid central manager (nil)"); + Q_ASSERT_X(peripheral, "-connectToPeripheral", "invalid peripheral (nil)"); + Q_ASSERT_X(managerState == OSXBluetooth::CentralManagerIdle, + "-connectToPeripheral", "invalid state"); + + // The state is still the same - connecting. + if ([self isConnected]) { + qCDebug(QT_BT_OSX) << "-connectToPeripheral, already connected"; + delegate->connectSuccess(); + } else { + qCDebug(QT_BT_OSX) << "-connectToPeripheral, trying to connect"; + managerState = OSXBluetooth::CentralManagerConnecting; + [manager connectPeripheral:peripheral options:nil]; + } +} + +- (bool)isConnected +{ + if (!peripheral) + return false; + +#if QT_MAC_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_9, __IPHONE_7_0) + return peripheral.state == CBPeripheralStateConnected; +#else + return peripheral.isConnected; +#endif +} + +- (void)disconnectFromDevice +{ + if (managerState == OSXBluetooth::CentralManagerUpdating) { + disconnectPending = true; + } else { + disconnectPending = false; + + if ([self isConnected]) { + Q_ASSERT_X(peripheral, "-disconnectFromDevice", "invalid peripheral (nil)"); + Q_ASSERT_X(manager, "-disconnectFromDevice", "invalid central manager (nil)"); + managerState = OSXBluetooth::CentralManagerDisconnecting; + [manager cancelPeripheralConnection:peripheral]; + } else { + managerState = OSXBluetooth::CentralManagerIdle; + delegate->disconnected(); + } + } +} + +- (void)discoverServices +{ +} + +- (bool)discoverServiceDetails:(const QBluetoothUuid &)serviceUuid +{ + Q_UNUSED(serviceUuid) + return false; +} + +- (bool)discoverCharacteristics:(const QBluetoothUuid &)serviceUuid +{ + Q_UNUSED(serviceUuid) + return false; +} + +// CBCentralManagerDelegate (the real one). + +- (void)centralManagerDidUpdateState:(CBCentralManager *)central +{ + Q_ASSERT_X(delegate, "-centralManagerDidUpdateState:", "invalid delegate (null)"); + + using namespace OSXBluetooth; + + const CBCentralManagerState state = central.state; + + if (state == CBCentralManagerStateUnknown + || state == CBCentralManagerStateResetting) { + // 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; + } + + if (disconnectPending) { + disconnectPending = false; + managerState = OSXBluetooth::CentralManagerIdle; + return [self disconnectFromDevice]; + } + + // Let's check some states we do not like first: + if (state == CBCentralManagerStateUnsupported || state == CBCentralManagerStateUnauthorized) { + if (managerState == CentralManagerUpdating) { + // We tried to connect just to realize, LE is not supported. Report this. + managerState = CentralManagerIdle; + delegate->LEnotSupported(); + } else { + // TODO: if we are here, LE _was_ supported and we first managed to update + // and reset managerState from CentralManagerUpdating. + managerState = CentralManagerIdle; + delegate->error(QLowEnergyController::InvalidBluetoothAdapterError); + } + return; + } + + if (state == CBCentralManagerStatePoweredOff) { + if (managerState == CentralManagerUpdating) { + // I've seen this instead of Unsopported on OS X. + managerState = CentralManagerIdle; + delegate->LEnotSupported(); + } else { + managerState = CentralManagerIdle; + // TODO: we need a better error + + // what will happen if later the state changes to PoweredOn??? + delegate->error(QLowEnergyController::InvalidBluetoothAdapterError); + } + return; + } + + if (state == CBCentralManagerStatePoweredOn) { + if (managerState == CentralManagerUpdating) { + managerState = CentralManagerIdle; + const QLowEnergyController::Error status = [self connectToDevice]; + if (status != QLowEnergyController::NoError)// An allocation problem? + delegate->error(status); + } + } else { + // We actually handled all known states, but .. Core Bluetooth can change? + Q_ASSERT_X(0, "-centralManagerDidUpdateState:", "invalid centra's state"); + } +} + +- (void)centralManager:(CBCentralManager *)central didRetrievePeripherals:(NSArray *)peripherals +{ + Q_UNUSED(central) + + // This method is required for iOS before 7.0 and OS X below 10.9. + Q_ASSERT_X(manager, "-centralManager:didRetrivePeripherals:", "invalid central manager (nil)"); + + if (managerState != OSXBluetooth::CentralManagerConnecting) { + // Canceled by calling -disconnectFromDevice method. + return; + } + + managerState = OSXBluetooth::CentralManagerIdle; + + if (!peripherals || peripherals.count != 1) { + Q_ASSERT_X(delegate, "-centralManager:didRetrievePeripherals:", + "invalid delegate (null)"); + + qCDebug(QT_BT_OSX) << "-centralManager:didRetrievePeripherals:, " + "unexpected number of peripherals (!= 1)"; + + delegate->error(QLowEnergyController::UnknownRemoteDeviceError); + } else { + peripheral = [static_cast<CBPeripheral *>([peripherals objectAtIndex:0]) retain]; + [self connectToPeripheral]; + } +} + +- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)aPeripheral +{ + Q_UNUSED(central) + Q_UNUSED(aPeripheral) + + Q_ASSERT_X(delegate, "-centralManager:didConnectPeripheral:", + "invalid delegate (null)"); + + if (managerState != OSXBluetooth::CentralManagerConnecting) { + // We called cancel but before disconnected, managed to connect? + return; + } + + managerState = OSXBluetooth::CentralManagerIdle; + delegate->connectSuccess(); +} + +- (void)centralManager:(CBCentralManager *)central didFailToConnectPeripheral:(CBPeripheral *)aPeripheral + error:(NSError *)error +{ + Q_UNUSED(central) + Q_UNUSED(aPeripheral) + Q_UNUSED(error) + + Q_ASSERT_X(delegate, "-centralManager:didFailToConnectPeripheral:", + "invalid delegate (null)"); + + if (managerState != OSXBluetooth::CentralManagerConnecting) { + // Canceled already. + return; + } + + managerState = OSXBluetooth::CentralManagerIdle; + // TODO: better error mapping is required. + delegate->error(QLowEnergyController::UnknownRemoteDeviceError); +} + +- (void)centralManager:(CBCentralManager *)central didDisconnectPeripheral:(CBPeripheral *)aPeripheral + error:(NSError *)error +{ + Q_UNUSED(central) + Q_UNUSED(aPeripheral) + + Q_ASSERT_X(delegate, "-centralManager:didDisconnectPeripheral:error:", + "invalid delegate (null)"); + + if (error && managerState == OSXBluetooth::CentralManagerDisconnecting) { + managerState = OSXBluetooth::CentralManagerIdle; + qCWarning(QT_BT_OSX) << "-centralManager:didDisconnectPeripheral:, " + "failed to disconnect"; + // TODO: instead of 'unknown' - .. ? + delegate->error(QLowEnergyController::UnknownRemoteDeviceError); + } else { + managerState = OSXBluetooth::CentralManagerIdle; + delegate->disconnected(); + } +} + +// CBPeripheralDelegate. + +- (void)peripheral:(CBPeripheral *)aPeripheral didDiscoverServices:(NSError *)error +{ + Q_UNUSED(aPeripheral) + Q_UNUSED(error) +} + + +- (void)peripheral:(CBPeripheral *)aPeripheral didDiscoverIncludedServicesForService:(CBService *)service + error:(NSError *)error +{ + Q_UNUSED(aPeripheral) + Q_UNUSED(service) + Q_UNUSED(error) +} + +- (void)peripheral:(CBPeripheral *)aPeripheral didDiscoverCharacteristicsForService:(CBService *)service + error:(NSError *)error +{ + Q_UNUSED(aPeripheral) + Q_UNUSED(service) + Q_UNUSED(error) +} + +@end diff --git a/src/bluetooth/osx/osxbtcentralmanager_p.h b/src/bluetooth/osx/osxbtcentralmanager_p.h new file mode 100644 index 00000000..5f836f42 --- /dev/null +++ b/src/bluetooth/osx/osxbtcentralmanager_p.h @@ -0,0 +1,128 @@ +/**************************************************************************** +** +** Copyright (C) 2014 Digia Plc and/or its subsidiary(-ies). +** Contact: http://www.qt-project.org/legal +** +** 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 Digia. For licensing terms and +** conditions see http://qt.digia.com/licensing. For further information +** use the contact form at http://qt.digia.com/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 2.1 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 2.1 requirements +** will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html. +** +** In addition, as a special exception, Digia gives you certain additional +** rights. These rights are described in the Digia Qt LGPL Exception +** version 1.1, included in the file LGPL_EXCEPTION.txt in this package. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3.0 as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL included in the +** packaging of this file. Please review the following information to +** ensure the GNU General Public License version 3.0 requirements will be +** met: http://www.gnu.org/copyleft/gpl.html. +** +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef OSXBTCENTRALMANAGER_P_H +#define OSXBTCENTRALMANAGER_P_H + +#include "qlowenergycontroller.h" +#include "qbluetoothuuid.h" +#include "osxbtutility_p.h" + +#include <QtCore/qglobal.h> + +// Foundation.h must be included before corebluetoothwrapper_p.h - +// a workaround for a broken 10.9 SDK. +#include <Foundation/Foundation.h> + +#include "corebluetoothwrapper_p.h" + +@class QT_MANGLE_NAMESPACE(OSXBTCentralManager); + +QT_BEGIN_NAMESPACE + +namespace OSXBluetooth { + +class CentralManagerDelegate +{ +public: + typedef QT_MANGLE_NAMESPACE(OSXBTCentralManager) ObjCCentralManager; + typedef ObjCStrongReference<NSArray> LEServices; + typedef LEServices LECharacteristics; + + virtual ~CentralManagerDelegate(); + + virtual void LEnotSupported() = 0; + virtual void connectSuccess() = 0; + virtual void serviceDiscoveryFinished(LEServices services) = 0; + virtual void includedServicesDiscoveryFinished(const QBluetoothUuid &serviceUuid, + LEServices services) = 0; + virtual void characteristicsDiscoveryFinished(const QBluetoothUuid &serviceUuid, + LECharacteristics characteristics) = 0; + virtual void disconnected() = 0; + + // General errors. + virtual void error(QLowEnergyController::Error error) = 0; + // Service related errors. + virtual void error(const QBluetoothUuid &serviceUuid, + QLowEnergyController::Error error) = 0; +}; + +enum CentralManagerState +{ + // QLowEnergyController already has some of these states, + // but it's not enough and we need more special states here. + CentralManagerIdle, + // Required by CBCentralManager to avoid problems with dangled pointers. + CentralManagerUpdating, + CentralManagerConnecting, + CentralManagerDiscovering, + CentralManagerDisconnecting +}; + +} + +QT_END_NAMESPACE + +@interface QT_MANGLE_NAMESPACE(OSXBTCentralManager) : NSObject<CBCentralManagerDelegate, CBPeripheralDelegate> +{ + CBCentralManager *manager; + QT_PREPEND_NAMESPACE(OSXBluetooth)::CentralManagerState managerState; + bool disconnectPending; + + QBluetoothUuid deviceUuid; + CBPeripheral *peripheral; + + QT_PREPEND_NAMESPACE(OSXBluetooth)::CentralManagerDelegate *delegate; +} + +- (id)initWithDelegate:(QT_PREPEND_NAMESPACE(OSXBluetooth)::CentralManagerDelegate *)aDelegate; +- (void)dealloc; + +- (QLowEnergyController::Error)connectToDevice:(const QBluetoothUuid &)aDeviceUuid; +- (void)disconnectFromDevice; + +- (void)discoverServices; +- (bool)discoverServiceDetails:(const QBluetoothUuid &)serviceUuid; +- (bool)discoverCharacteristics:(const QBluetoothUuid &)serviceUuid; + +@end + +#endif diff --git a/src/bluetooth/qlowenergycontroller_osx.mm b/src/bluetooth/qlowenergycontroller_osx.mm index 08721933..6e62844c 100644 --- a/src/bluetooth/qlowenergycontroller_osx.mm +++ b/src/bluetooth/qlowenergycontroller_osx.mm @@ -53,23 +53,39 @@ QT_BEGIN_NAMESPACE QLowEnergyControllerPrivateOSX::QLowEnergyControllerPrivateOSX(QLowEnergyController *q) : q_ptr(q), + isConnecting(false), lastError(QLowEnergyController::NoError), controllerState(QLowEnergyController::UnconnectedState), addressType(QLowEnergyController::PublicAddress) { - Q_ASSERT_X(q, "QLowEnergyControllerPrivate", "invalid q_ptr (null)"); // This is the "wrong" constructor - no valid device UUID to connect later. + Q_ASSERT_X(q, "QLowEnergyControllerPrivate", "invalid q_ptr (null)"); + // We still create a manager, to simplify error handling later. + centralManager.reset([[ObjCCentralManager alloc] initWithDelegate:this]); + if (!centralManager) { + qCWarning(QT_BT_OSX) << "QBluetoothLowEnergyControllerPrivateOSX::" + "QBluetoothLowEnergyControllerPrivateOSX(), " + "failed to initialize central manager"; + } + } QLowEnergyControllerPrivateOSX::QLowEnergyControllerPrivateOSX(QLowEnergyController *q, const QBluetoothDeviceInfo &deviceInfo) : q_ptr(q), deviceUuid(deviceInfo.deviceUuid()), + isConnecting(false), lastError(QLowEnergyController::NoError), controllerState(QLowEnergyController::UnconnectedState), addressType(QLowEnergyController::PublicAddress) { Q_ASSERT_X(q, "QLowEnergyControllerPrivateOSX", "invalid q_ptr (null)"); + centralManager.reset([[ObjCCentralManager alloc] initWithDelegate:this]); + if (!centralManager) { + qCWarning(QT_BT_OSX) << "QBluetoothLowEnergyControllerPrivateOSX::" + "QBluetoothLowEnergyControllerPrivateOSX(), " + "failed to initialize central manager"; + } } QLowEnergyControllerPrivateOSX::~QLowEnergyControllerPrivateOSX() @@ -78,11 +94,136 @@ QLowEnergyControllerPrivateOSX::~QLowEnergyControllerPrivateOSX() bool QLowEnergyControllerPrivateOSX::isValid() const { - return false; + // isValid means only "was able to allocate all resources", + // nothing more. + return centralManager; +} + +void QLowEnergyControllerPrivateOSX::LEnotSupported() +{ + // Report as an error. But this should not be possible + // actually: before connecting to any device, we have + // to discover it, if it was discovered ... LE _must_ + // be supported. +} + +void QLowEnergyControllerPrivateOSX::connectSuccess() +{ + Q_ASSERT_X(controllerState == QLowEnergyController::ConnectingState, + "connectSuccess", "invalid state"); + + controllerState = QLowEnergyController::ConnectedState; + + if (!isConnecting) { + emit q_ptr->stateChanged(QLowEnergyController::ConnectedState); + emit q_ptr->connected(); + } +} + +void QLowEnergyControllerPrivateOSX::serviceDiscoveryFinished(LEServices services) +{ + Q_UNUSED(services) +} + +void QLowEnergyControllerPrivateOSX::includedServicesDiscoveryFinished(const QBluetoothUuid &serviceUuid, + LEServices services) +{ + Q_UNUSED(serviceUuid) + Q_UNUSED(services) +} + +void QLowEnergyControllerPrivateOSX::characteristicsDiscoveryFinished(const QBluetoothUuid &serviceUuid, + LECharacteristics characteristics) +{ + Q_UNUSED(serviceUuid) + Q_UNUSED(characteristics) +} + + +void QLowEnergyControllerPrivateOSX::disconnected() +{ + controllerState = QLowEnergyController::UnconnectedState; + + if (!isConnecting) { + emit q_ptr->stateChanged(QLowEnergyController::UnconnectedState); + emit q_ptr->disconnected(); + } +} + +void QLowEnergyControllerPrivateOSX::error(QLowEnergyController::Error errorCode) +{ + // Errors reported during connect and general errors. + lastError = errorCode; + + // We're still in connectToDevice, + // some error was reported synchronously. + // Return, the error will be correctly set later + // by connectToDevice. + if (isConnecting) + return; + + switch (lastError) { + case QLowEnergyController::UnknownRemoteDeviceError: + errorString = QLowEnergyController::tr("Remote device cannot be found"); + break; + case QLowEnergyController::InvalidBluetoothAdapterError: + errorString = QLowEnergyController::tr("Cannot find local adapter"); + break; + case QLowEnergyController::NetworkError: + errorString = QLowEnergyController::tr("Error occurred during connection I/O"); + break; + case QLowEnergyController::UnknownError: + default: + errorString = QLowEnergyController::tr("Unknown Error"); + break; + } + + emit q_ptr->error(lastError); +} + +void QLowEnergyControllerPrivateOSX::error(const QBluetoothUuid &serviceUuid, + QLowEnergyController::Error errorCode) +{ + // Service/characteristics-related errors. + Q_UNUSED(serviceUuid) + Q_UNUSED(errorCode) } void QLowEnergyControllerPrivateOSX::connectToDevice() { + Q_ASSERT_X(isValid(), "connectToDevice", "invalid private controller"); + Q_ASSERT_X(controllerState == QLowEnergyController::UnconnectedState, + "connectToDevice", "invalid state"); + Q_ASSERT_X(!deviceUuid.isNull(), "connectToDevice", + "invalid private controller (no device uuid)"); + Q_ASSERT_X(!isConnecting, "connectToDevice", + "recursive connectToDevice call"); + + lastError = QLowEnergyController::NoError; + errorString.clear(); + + isConnecting = true;// Do not emit signals if some callback is executed synchronously. + controllerState = QLowEnergyController::ConnectingState; + const QLowEnergyController::Error status = [centralManager connectToDevice:deviceUuid]; + isConnecting = false; + + if (status == QLowEnergyController::NoError && lastError == QLowEnergyController::NoError) { + emit q_ptr->stateChanged(controllerState); + if (controllerState == QLowEnergyController::ConnectedState) { + // If a peripheral is connected already from the Core Bluetooth's + // POV: + emit q_ptr->connected(); + } else if (controllerState == QLowEnergyController::UnconnectedState) { + // Ooops, tried to connect, got peripheral disconnect instead - + // this happens with Core Bluetooth. + emit q_ptr->disconnected(); + } + } else if (status != QLowEnergyController::NoError) { + error(status); + } else { + // Re-set the error/description and emit. + error(lastError); + } } void QLowEnergyControllerPrivateOSX::discoverServices() @@ -100,6 +241,9 @@ QLowEnergyController::QLowEnergyController(const QBluetoothAddress &remoteAddres d_ptr(new QLowEnergyControllerPrivateOSX(this)) { Q_UNUSED(remoteAddress) + + qCWarning(QT_BT_OSX) << "QLowEnergyController::QLowEnergyController(), " + "construction with remote address is not supported!"; } QLowEnergyController::QLowEnergyController(const QBluetoothDeviceInfo &remoteDevice, @@ -107,7 +251,8 @@ QLowEnergyController::QLowEnergyController(const QBluetoothDeviceInfo &remoteDev : QObject(parent), d_ptr(new QLowEnergyControllerPrivateOSX(this, remoteDevice)) { - Q_UNUSED(remoteDevice) + // That's the only "real" ctor - with Core Bluetooth we need a _valid_ deviceUuid + // from 'remoteDevice'. } QLowEnergyController::QLowEnergyController(const QBluetoothAddress &remoteAddress, @@ -122,12 +267,12 @@ QLowEnergyController::QLowEnergyController(const QBluetoothAddress &remoteAddres osx_d_ptr->localAddress = localAddress; qCWarning(QT_BT_OSX) << "QLowEnergyController::QLowEnergyController(), " - "construction with remote address is not supported"; + "construction with remote/local addresses is not supported!"; } QLowEnergyController::~QLowEnergyController() { - // TODO: disconnect, but this can be quite problematic - it's async. operation! + // Deleting a peripheral will also disconnect. delete d_ptr; } @@ -170,10 +315,34 @@ void QLowEnergyController::setRemoteAddressType(RemoteAddressType type) void QLowEnergyController::connectToDevice() { + OSX_D_PTR; + + // A memory allocation problem. + if (!osx_d_ptr->isValid()) + return osx_d_ptr->error(UnknownError); + + // No QBluetoothDeviceInfo provided during construction. + if (!osx_d_ptr->deviceUuid.isNull()) + return osx_d_ptr->error(UnknownRemoteDeviceError); + + if (osx_d_ptr->controllerState != UnconnectedState) + return; + + osx_d_ptr->connectToDevice(); } void QLowEnergyController::disconnectFromDevice() { + if (state() == UnconnectedState || state() == ClosingState) + return; + + OSX_D_PTR; + + if (osx_d_ptr->isValid()) { + osx_d_ptr->controllerState = ClosingState; + emit stateChanged(ClosingState); + [osx_d_ptr->centralManager disconnectFromDevice]; + } } void QLowEnergyController::discoverServices() diff --git a/src/bluetooth/qlowenergycontroller_osx_p.h b/src/bluetooth/qlowenergycontroller_osx_p.h index 6d48f591..4162c764 100644 --- a/src/bluetooth/qlowenergycontroller_osx_p.h +++ b/src/bluetooth/qlowenergycontroller_osx_p.h @@ -34,6 +34,7 @@ #ifndef QLOWENERGYCONTROLLER_OSX_P_H #define QLOWENERGYCONTROLLER_OSX_P_H +#include "osx/osxbtcentralmanager_p.h" #include "qlowenergycontroller_p.h" #include "qlowenergycontroller.h" #include "osx/osxbtutility_p.h" @@ -47,7 +48,8 @@ QT_BEGIN_NAMESPACE // The suffix OSX is not the very right, it's also iOS. -class QLowEnergyControllerPrivateOSX : public QLowEnergyControllerPrivate +class QLowEnergyControllerPrivateOSX : public QLowEnergyControllerPrivate, + public OSXBluetooth::CentralManagerDelegate { friend class QLowEnergyController; public: @@ -59,12 +61,32 @@ public: bool isValid() const; private: + // CentralManagerDelegate: + void LEnotSupported() Q_DECL_OVERRIDE; + void connectSuccess() Q_DECL_OVERRIDE; + + void serviceDiscoveryFinished(LEServices services) Q_DECL_OVERRIDE; + void includedServicesDiscoveryFinished(const QBluetoothUuid &serviceUuid, + LEServices services) Q_DECL_OVERRIDE; + void characteristicsDiscoveryFinished(const QBluetoothUuid &serviceUuid, + LECharacteristics characteristics) Q_DECL_OVERRIDE; + void disconnected() Q_DECL_OVERRIDE; + void error(QLowEnergyController::Error errorCode) Q_DECL_OVERRIDE; + void error(const QBluetoothUuid &serviceUuid, + QLowEnergyController::Error errorCode) Q_DECL_OVERRIDE; + void connectToDevice(); void discoverServices(); void discoverServiceDetails(const QBluetoothUuid &serviceUuid); QLowEnergyController *q_ptr; QBluetoothUuid deviceUuid; + // To be sure we set controller's state correctly + // (Connecting or Connected) we have to know if we're + // still inside connectToDevice - this is important, + // if a peripheral is _already_ connected from Core Bluetooth's + // point of view. + bool isConnecting; QString errorString; QLowEnergyController::Error lastError; @@ -75,6 +97,9 @@ private: QLowEnergyController::ControllerState controllerState; QLowEnergyController::RemoteAddressType addressType; + typedef OSXBluetooth::ObjCScopedPointer<ObjCCentralManager> CentralManager; + CentralManager centralManager; + typedef QMap<QBluetoothUuid, QSharedPointer<QLowEnergyServicePrivate> > ServiceMap; typedef ServiceMap::const_iterator ConstServiceIterator; typedef ServiceMap::iterator ServiceIterator; |