diff options
Diffstat (limited to 'src/bluetooth/darwin')
37 files changed, 9043 insertions, 0 deletions
diff --git a/src/bluetooth/darwin/btcentralmanager.mm b/src/bluetooth/darwin/btcentralmanager.mm new file mode 100644 index 00000000..dd3cbb73 --- /dev/null +++ b/src/bluetooth/darwin/btcentralmanager.mm @@ -0,0 +1,1860 @@ +/**************************************************************************** +** +** 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 "btcentralmanager_p.h" +#include "btnotifier_p.h" + +#include <QtCore/qloggingcategory.h> +#include <QtCore/qdebug.h> + +#include <algorithm> +#include <vector> +#include <limits> + +Q_DECLARE_METATYPE(QLowEnergyHandle) + +QT_BEGIN_NAMESPACE + +namespace DarwinBluetooth { + +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 DarwinBluetooth + +QT_END_NAMESPACE + +QT_USE_NAMESPACE + +@interface QT_MANGLE_NAMESPACE(DarwinBTCentralManager) (PrivateAPI) + +- (void)watchAfter:(id)object timeout:(DarwinBluetooth::OperationTimeout)type; +- (bool)objectIsUnderWatch:(id)object operation:(DarwinBluetooth::OperationTimeout)type; +- (void)stopWatchingAfter:(id)object operation:(DarwinBluetooth::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(DarwinBTCentralManager) +{ +@private + CBCentralManager *manager; + DarwinBluetooth::CentralManagerState managerState; + bool disconnectPending; + + QBluetoothUuid deviceUuid; + + DarwinBluetooth::LECBManagerNotifier *notifier; + + // Quite a verbose service discovery machinery + // (a "graph traversal"). + DarwinBluetooth::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': + DarwinBluetooth::ObjCStrongReference<NSMutableArray> servicesToVisitNext; + // We'd like to avoid loops in a services' topology: + DarwinBluetooth::ObjCStrongReference<NSMutableSet> visitedServices; + + QList<QBluetoothUuid> servicesToDiscoverDetails; + + DarwinBluetooth::ServiceHash serviceMap; + DarwinBluetooth::CharHash charMap; + DarwinBluetooth::DescHash descMap; + + QLowEnergyHandle lastValidHandle; + + bool requestPending; + DarwinBluetooth::RequestQueue requests; + QLowEnergyHandle currentReadHandle; + + DarwinBluetooth::ValueHash valuesToWrite; + + qint64 timeoutMS; + std::vector<DarwinBluetooth::GCDTimer> timeoutWatchdogs; + + CBPeripheral *peripheral; +} + +- (id)initWith:(DarwinBluetooth::LECBManagerNotifier *)aNotifier +{ + using namespace DarwinBluetooth; + + 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:(DarwinBluetooth::OperationTimeout)type +{ + using namespace DarwinBluetooth; + + 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:(DarwinBluetooth::OperationTimeout)type +{ + return DarwinBluetooth::qt_find_watchdog(timeoutWatchdogs, object, type) != timeoutWatchdogs.end(); +} + +- (void)stopWatchingAfter:(id)object operation:(DarwinBluetooth::OperationTimeout)type +{ + auto pos = DarwinBluetooth::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 DarwinBluetooth; + + 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_DARWIN, "Timeout in services discovery"); + [self peripheral:peripheral didDiscoverServices:nsError]; + break; + case OperationTimeout::includedServicesDiscovery: + qCWarning(QT_BT_DARWIN, "Timeout in included services discovery"); + [self peripheral:peripheral didDiscoverIncludedServicesForService:cbObject error:nsError]; + break; + case OperationTimeout::characteristicsDiscovery: + qCWarning(QT_BT_DARWIN, "Timeout in characteristics discovery"); + [self peripheral:peripheral didDiscoverCharacteristicsForService:cbObject error:nsError]; + break; + case OperationTimeout::characteristicRead: + qCWarning(QT_BT_DARWIN, "Timeout while reading a characteristic"); + [self peripheral:peripheral didUpdateValueForCharacteristic:cbObject error:nsError]; + break; + case OperationTimeout::descriptorsDiscovery: + qCWarning(QT_BT_DARWIN, "Timeout in descriptors discovery"); + [self peripheral:peripheral didDiscoverDescriptorsForCharacteristic:cbObject error:nsError]; + break; + case OperationTimeout::descriptorRead: + qCWarning(QT_BT_DARWIN, "Timeout while reading a descriptor"); + [self peripheral:peripheral didUpdateValueForDescriptor:cbObject error:nsError]; + break; + case OperationTimeout::characteristicWrite: + qCWarning(QT_BT_DARWIN, "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 = DarwinBluetooth::qt_LE_queue()) { + managerState = DarwinBluetooth::CentralManagerUpdating; + manager = [[CBCentralManager alloc] initWithDelegate:self queue:leQueue]; + } + + if (!manager) { + managerState = DarwinBluetooth::CentralManagerIdle; + qCWarning(QT_BT_DARWIN) << "failed to allocate a central manager"; + if (notifier) + emit notifier->CBManagerError(QLowEnergyController::ConnectionError); + } + } else if (managerState != DarwinBluetooth::CentralManagerUpdating) { + [self retrievePeripheralAndConnect]; + } +} + +- (void)retrievePeripheralAndConnect +{ + Q_ASSERT_X(manager, Q_FUNC_INFO, "invalid central manager (nil)"); + Q_ASSERT_X(managerState == DarwinBluetooth::CentralManagerIdle, + Q_FUNC_INFO, "invalid state"); + + if ([self isConnected]) { + qCDebug(QT_BT_DARWIN) << "already connected"; + if (notifier) + emit notifier->connected(); + return; + } else if (peripheral) { + // Was retrieved already, but not connected + // or disconnected. + [self connectToPeripheral]; + return; + } + + using namespace DarwinBluetooth; + + // Retrieve a peripheral first ... + ObjCScopedPointer<NSMutableArray> uuids([[NSMutableArray alloc] init]); + if (!uuids) { + qCWarning(QT_BT_DARWIN) << "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_DARWIN) << "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_DARWIN) << "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 DarwinBluetooth; + + 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_DARWIN) << "already connected"; + if (notifier) + emit notifier->connected(); + } else { + qCDebug(QT_BT_DARWIN) << "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 == DarwinBluetooth::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 = DarwinBluetooth::CentralManagerDisconnecting; + else + managerState = DarwinBluetooth::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 DarwinBluetooth; + + 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 DarwinBluetooth; + + 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 DarwinBluetooth; + + 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_DARWIN) << "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_DARWIN) << "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 DarwinBluetooth; + + 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 DarwinBluetooth; + + 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 DarwinBluetooth; + + 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 DarwinBluetooth; + + 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_DARWIN) << "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 DarwinBluetooth; + + 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 DarwinBluetooth; + + 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_DARWIN) << "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_DARWIN) << "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 DarwinBluetooth; + + 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_DARWIN) << "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_DARWIN) << "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_DARWIN) << "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_DARWIN) << "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 DarwinBluetooth; + + Q_ASSERT_X(charHandle, Q_FUNC_INFO, "invalid characteristic handle (0)"); + + if (!charMap.contains(charHandle)) { + qCWarning(QT_BT_DARWIN) << "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_DARWIN) << "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 DarwinBluetooth; + + Q_ASSERT_X(charHandle, Q_FUNC_INFO, "invalid characteristic handle (0)"); + + QT_BT_MAC_AUTORELEASEPOOL; + + if (!charMap.contains(charHandle)) { + qCWarning(QT_BT_DARWIN) << "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 DarwinBluetooth; + + Q_ASSERT_X(charHandle, Q_FUNC_INFO, "invalid characteristic handle (0)"); + + QT_BT_MAC_AUTORELEASEPOOL; + + if (!charMap.contains(charHandle)) { + qCWarning(QT_BT_DARWIN) << "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 DarwinBluetooth; + + Q_ASSERT_X(descHandle, Q_FUNC_INFO, "invalid descriptor handle (0)"); + + if (!descMap.contains(descHandle)) { + qCWarning(QT_BT_DARWIN) << "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 DarwinBluetooth; + + Q_ASSERT_X(descHandle, Q_FUNC_INFO, "invalid descriptor handle (0)"); + + if (!descMap.contains(descHandle)) { + qCWarning(QT_BT_DARWIN) << "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 DarwinBluetooth; + + 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 (DarwinBluetooth::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_DARWIN) << "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_DARWIN) << "unexpected descriptor, no handle found"; + return false; + } + } else { + qCWarning(QT_BT_DARWIN) << "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_DARWIN) << "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 DarwinBluetooth; + +#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 != DarwinBluetooth::CentralManagerConnecting) { + // We called cancel but before disconnected, managed to connect? + return; + } + + managerState = DarwinBluetooth::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 != DarwinBluetooth::CentralManagerConnecting) { + // Canceled already. + return; + } + + managerState = DarwinBluetooth::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 == DarwinBluetooth::CentralManagerDisconnecting) { + managerState = DarwinBluetooth::CentralManagerIdle; + qCWarning(QT_BT_DARWIN) << "failed to disconnect"; + if (notifier) + emit notifier->CBManagerError(QLowEnergyController::UnknownRemoteDeviceError); + } else { + managerState = DarwinBluetooth::CentralManagerIdle; + if (notifier) + emit notifier->disconnected(); + } +} + +// CBPeripheralDelegate. + +- (void)peripheral:(CBPeripheral *)aPeripheral didDiscoverServices:(NSError *)error +{ + Q_UNUSED(aPeripheral) + + using namespace DarwinBluetooth; + + if (managerState != CentralManagerDiscovering) { + // Canceled by -disconnectFromDevice, or as a result of a timeout. + return; + } + + if (![self objectIsUnderWatch:aPeripheral operation:OperationTimeout::serviceDiscovery]) // Timed out already + return; + + QT_BT_MAC_AUTORELEASEPOOL; + + [self stopWatchingAfter:aPeripheral operation:OperationTimeout::serviceDiscovery]; + + managerState = 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_DARWIN) << "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 = DarwinBluetooth::CentralManagerIdle; + if (notifier) + emit notifier->servicesWereModified(); +} + +- (void)peripheral:(CBPeripheral *)aPeripheral didDiscoverIncludedServicesForService:(CBService *)service + error:(NSError *)error +{ + Q_UNUSED(aPeripheral) + + using namespace DarwinBluetooth; + + 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 DarwinBluetooth; + + 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 DarwinBluetooth; + + 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_DARWIN) << "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 DarwinBluetooth; + + 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 DarwinBluetooth; + + 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_DARWIN) << "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 DarwinBluetooth; + + 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_DARWIN) << "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 DarwinBluetooth; + + 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_DARWIN) << "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 DarwinBluetooth; + + 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 diff --git a/src/bluetooth/darwin/btcentralmanager_p.h b/src/bluetooth/darwin/btcentralmanager_p.h new file mode 100644 index 00000000..0b5c8fbb --- /dev/null +++ b/src/bluetooth/darwin/btcentralmanager_p.h @@ -0,0 +1,175 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#ifndef BTCENTRALMANAGER_P_H +#define BTCENTRALMANAGER_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qlowenergycontroller.h" +#include "qlowenergyservice.h" +#include "qbluetoothuuid.h" +#include "btgcdtimer_p.h" +#include "btutility_p.h" + +#include <QtCore/qbytearray.h> +#include <QtCore/qglobal.h> +#include <QtCore/qqueue.h> +#include <QtCore/qhash.h> + +#include <Foundation/Foundation.h> + +#include <CoreBluetooth/CoreBluetooth.h> + +QT_BEGIN_NAMESPACE + +class QLowEnergyServicePrivate; + +namespace DarwinBluetooth { + +class LECBManagerNotifier; + +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 +}; + +// In Qt we work with handles and UUIDs. Core Bluetooth +// has NSArrays (and nested NSArrays inside servces/characteristics). +// To simplify a navigation, I need a simple way to map from a handle +// to a Core Bluetooth object. These are weak pointers, +// will probably require '__weak' with ARC. +typedef QHash<QLowEnergyHandle, CBService *> ServiceHash; +typedef QHash<QLowEnergyHandle, CBCharacteristic *> CharHash; +typedef QHash<QLowEnergyHandle, CBDescriptor *> DescHash; + +// Descriptor/charactesirstic read/write requests +// - we have to serialize 'concurrent' requests. +struct LERequest { + enum RequestType { + CharRead, + CharWrite, + DescRead, + DescWrite, + ClientConfiguration, + Invalid + }; + + LERequest() : type(Invalid), + withResponse(false), + handle(0) + {} + + RequestType type; + bool withResponse; + QLowEnergyHandle handle; + QByteArray value; +}; + +typedef QQueue<LERequest> RequestQueue; + +// Core Bluetooth's write confirmation does not provide +// the updated value (characteristic or descriptor). +// But the Qt's Bluetooth API ('write with response') +// expects this updated value as a response, so we have +// to cache this write value and report it back. +// 'NSObject *' will require '__weak' with ARC. +typedef QHash<NSObject *, QByteArray> ValueHash; + +} // namespace DarwinBluetooth + +QT_END_NAMESPACE + +@interface QT_MANGLE_NAMESPACE(DarwinBTCentralManager) : NSObject<CBCentralManagerDelegate, + CBPeripheralDelegate, + QT_MANGLE_NAMESPACE(GCDTimerDelegate)> +- (id)initWith:(QT_PREPEND_NAMESPACE(DarwinBluetooth)::LECBManagerNotifier *)notifier; +- (void)dealloc; + +- (CBPeripheral *)peripheral; + +// IMPORTANT: _all_ these methods are to be executed on qt_LE_queue, +// when passing parameters - C++ objects _must_ be copied (see the controller's code). +- (void)connectToDevice:(const QT_PREPEND_NAMESPACE(QBluetoothUuid) &)aDeviceUuid; + +- (void)disconnectFromDevice; + +- (void)discoverServices; +- (void)discoverServiceDetails:(const QT_PREPEND_NAMESPACE(QBluetoothUuid) &)serviceUuid; + +- (void)setNotifyValue:(const QT_PREPEND_NAMESPACE(QByteArray) &)value + forCharacteristic:(QT_PREPEND_NAMESPACE(QLowEnergyHandle))charHandle + onService:(const QT_PREPEND_NAMESPACE(QBluetoothUuid) &)serviceUuid; + +- (void)readCharacteristic:(QT_PREPEND_NAMESPACE(QLowEnergyHandle))charHandle + onService:(const QT_PREPEND_NAMESPACE(QBluetoothUuid) &)serviceUuid; + +- (void)write:(const QT_PREPEND_NAMESPACE(QByteArray) &)value + charHandle:(QT_PREPEND_NAMESPACE(QLowEnergyHandle))charHandle + onService:(const QT_PREPEND_NAMESPACE(QBluetoothUuid) &)serviceUuid + withResponse:(bool)writeWithResponse; + +- (void)readDescriptor:(QT_PREPEND_NAMESPACE(QLowEnergyHandle))descHandle + onService:(const QT_PREPEND_NAMESPACE(QBluetoothUuid) &)serviceUuid; + +- (void)write:(const QT_PREPEND_NAMESPACE(QByteArray) &)value + descHandle:(QT_PREPEND_NAMESPACE(QLowEnergyHandle))descHandle + onService:(const QT_PREPEND_NAMESPACE(QBluetoothUuid) &)serviceUuid; + +- (void)detach; + +@end + +#endif diff --git a/src/bluetooth/darwin/btconnectionmonitor.mm b/src/bluetooth/darwin/btconnectionmonitor.mm new file mode 100644 index 00000000..f9adfde8 --- /dev/null +++ b/src/bluetooth/darwin/btconnectionmonitor.mm @@ -0,0 +1,133 @@ +/**************************************************************************** +** +** 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 "btconnectionmonitor_p.h" +#include "btutility_p.h" + +#include <QtCore/qdebug.h> + +QT_BEGIN_NAMESPACE + + +QT_END_NAMESPACE + +#ifdef QT_NAMESPACE +using namespace QT_NAMESPACE; +#endif + +@implementation QT_MANGLE_NAMESPACE(DarwinBTConnectionMonitor) +{ + QT_PREPEND_NAMESPACE(DarwinBluetooth::ConnectionMonitor) *monitor; + IOBluetoothUserNotification *discoveryNotification; + NSMutableArray *foundConnections; +} + +- (id)initWithMonitor:(DarwinBluetooth::ConnectionMonitor *)aMonitor +{ + Q_ASSERT_X(aMonitor, "-initWithMonitor:", "invalid monitor (null)"); + + if (self = [super init]) { + monitor = aMonitor; + discoveryNotification = [[IOBluetoothDevice registerForConnectNotifications:self + selector:@selector(connectionNotification:withDevice:)] retain]; + foundConnections = [[NSMutableArray alloc] init]; + } + + return self; +} + +- (void)dealloc +{ + [discoveryNotification unregister]; + [discoveryNotification release]; + + for (IOBluetoothUserNotification *n in foundConnections) + [n unregister]; + + [foundConnections release]; + + [super dealloc]; +} + +- (void)connectionNotification:(IOBluetoothUserNotification *)aNotification + withDevice:(IOBluetoothDevice *)device +{ + Q_UNUSED(aNotification) + + typedef IOBluetoothUserNotification Notification; + + if (!device) + return; + + QT_BT_MAC_AUTORELEASEPOOL; + + // All Obj-C objects are autoreleased. + + const QBluetoothAddress deviceAddress(DarwinBluetooth::qt_address([device getAddress])); + if (deviceAddress.isNull()) + return; + + if (foundConnections) { + Notification *const notification = [device registerForDisconnectNotification:self + selector: @selector(connectionClosedNotification:withDevice:)]; + if (notification) + [foundConnections addObject:notification]; + } + + Q_ASSERT_X(monitor, "-connectionNotification:withDevice:", "invalid monitor (null)"); + monitor->deviceConnected(deviceAddress); +} + +- (void)connectionClosedNotification:(IOBluetoothUserNotification *)notification + withDevice:(IOBluetoothDevice *)device +{ + QT_BT_MAC_AUTORELEASEPOOL; + + [notification unregister];//? + [foundConnections removeObject:notification]; + + const QBluetoothAddress deviceAddress(DarwinBluetooth::qt_address([device getAddress])); + if (deviceAddress.isNull()) + return; + + Q_ASSERT_X(monitor, "-connectionClosedNotification:withDevice:", "invalid monitor (null)"); + monitor->deviceDisconnected(deviceAddress); +} + +@end diff --git a/src/bluetooth/darwin/btconnectionmonitor_p.h b/src/bluetooth/darwin/btconnectionmonitor_p.h new file mode 100644 index 00000000..e3b7b90d --- /dev/null +++ b/src/bluetooth/darwin/btconnectionmonitor_p.h @@ -0,0 +1,71 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#ifndef BTCONNECTIONMONITOR_P_H +#define BTCONNECTIONMONITOR_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qbluetoothaddress.h" +#include "btdelegates_p.h" + +#include <QtCore/qglobal.h> + +#include <Foundation/Foundation.h> + +#include <IOBluetooth/IOBluetooth.h> + +@interface QT_MANGLE_NAMESPACE(DarwinBTConnectionMonitor) : NSObject + +- (id)initWithMonitor:(QT_PREPEND_NAMESPACE(DarwinBluetooth::ConnectionMonitor) *)monitor; +- (void)connectionNotification:(id)notification withDevice:(IOBluetoothDevice *)device; +- (void)connectionClosedNotification:(id)notification withDevice:(IOBluetoothDevice *)device; + +@end + +#endif diff --git a/src/bluetooth/darwin/btdelegates.cpp b/src/bluetooth/darwin/btdelegates.cpp new file mode 100644 index 00000000..531ca1df --- /dev/null +++ b/src/bluetooth/darwin/btdelegates.cpp @@ -0,0 +1,76 @@ +/**************************************************************************** +** +** Copyright (C) 2019 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 "btdelegates_p.h" + +#if defined(Q_OS_MACOS) + +QT_BEGIN_NAMESPACE + +namespace DarwinBluetooth { + +DeviceInquiryDelegate::~DeviceInquiryDelegate() +{ +} + +PairingDelegate::~PairingDelegate() +{ +} + +SDPInquiryDelegate::~SDPInquiryDelegate() +{ +} + +ChannelDelegate::~ChannelDelegate() +{ +} + +ConnectionMonitor::~ConnectionMonitor() +{ +} + +SocketListener::~SocketListener() +{ +} + +} // namespace DarwinBluetooth + +QT_END_NAMESPACE + +#endif diff --git a/src/bluetooth/darwin/btdelegates_p.h b/src/bluetooth/darwin/btdelegates_p.h new file mode 100644 index 00000000..11fbcc28 --- /dev/null +++ b/src/bluetooth/darwin/btdelegates_p.h @@ -0,0 +1,149 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtBluetooth module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef BTDELEGATES_P_H +#define BTDELEGATES_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qbluetoothdevicediscoveryagent.h" +#include "qlowenergycontroller.h" +#include "qbluetooth.h" + +#include <QtCore/qsharedpointer.h> +#include <QtCore/qglobal.h> + +#if defined(Q_OS_MACOS) + +#include <IOKit/IOReturn.h> + +#include <cstdint> + +QT_BEGIN_NAMESPACE + +class QLowEnergyServicePrivate; +class QBluetoothAddress; +class QByteArray; + +namespace DarwinBluetooth { + +class DeviceInquiryDelegate +{ +public: + virtual ~DeviceInquiryDelegate(); + + virtual void inquiryFinished() = 0; + virtual void error(IOReturn error) = 0; + virtual void classicDeviceFound(void *ioBluetoothDevice) = 0; +}; + +class PairingDelegate +{ +public: + using BluetoothNumericValue = uint32_t; + using BluetoothPasskey = BluetoothNumericValue; + + virtual ~PairingDelegate(); + + virtual void connecting(void *pair) = 0; + virtual void requestPIN(void *pair) = 0; + virtual void requestUserConfirmation(void *pair, + BluetoothNumericValue) = 0; + virtual void passkeyNotification(void *pair, + BluetoothPasskey passkey) = 0; + virtual void error(void *pair, IOReturn errorCode) = 0; + virtual void pairingFinished(void *pair) = 0; +}; + +class SDPInquiryDelegate { +public: + virtual ~SDPInquiryDelegate(); + + virtual void SDPInquiryFinished(void *ioBluetoothDevice) = 0; + virtual void SDPInquiryError(void *ioBluetoothDevice, IOReturn errorCode) = 0; +}; + +// L2CAP and RFCOMM. +class ChannelDelegate +{ +public: + virtual ~ChannelDelegate(); + + virtual void setChannelError(IOReturn errorCode) = 0; + virtual void channelOpenComplete() = 0; + virtual void channelClosed() = 0; + + virtual void readChannelData(void *data, std::size_t size) = 0; + virtual void writeComplete() = 0; +}; + +class ConnectionMonitor { +public: + virtual ~ConnectionMonitor(); + + virtual void deviceConnected(const QBluetoothAddress &address) = 0; + virtual void deviceDisconnected(const QBluetoothAddress &address) = 0; +}; + +class SocketListener +{ +public: + virtual ~SocketListener(); + + virtual void openNotifyRFCOMM(void *rfcommChannel) = 0; + virtual void openNotifyL2CAP(void *l2capChannel) = 0; +}; + + +} // namespace DarwinBluetooth + +QT_END_NAMESPACE + +#endif // Q_OS_MACOS + +#endif // DARWINBTDELEGATES_P_H diff --git a/src/bluetooth/darwin/btdeviceinquiry.mm b/src/bluetooth/darwin/btdeviceinquiry.mm new file mode 100644 index 00000000..ad59a4a4 --- /dev/null +++ b/src/bluetooth/darwin/btdeviceinquiry.mm @@ -0,0 +1,168 @@ +/**************************************************************************** +** +** 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 "btdeviceinquiry_p.h" +#include "btutility_p.h" + +#include <QtCore/qloggingcategory.h> +#include <QtCore/qdebug.h> + +QT_USE_NAMESPACE + +@implementation QT_MANGLE_NAMESPACE(DarwinBTClassicDeviceInquiry) +{ + IOBluetoothDeviceInquiry *m_inquiry; + bool m_active; + DarwinBluetooth::DeviceInquiryDelegate *m_delegate;//C++ "delegate" +} + +- (id)initWithDelegate:(DarwinBluetooth::DeviceInquiryDelegate *)delegate +{ + if (self = [super init]) { + Q_ASSERT_X(delegate, Q_FUNC_INFO, "invalid device inquiry delegate (null)"); + + m_inquiry = [[IOBluetoothDeviceInquiry inquiryWithDelegate:self] retain]; + + if (m_inquiry) { + [m_inquiry setInquiryLength:15]; + [m_inquiry setUpdateNewDeviceNames:NO];//Useless, disable! + m_delegate = delegate; + } else { + qCCritical(QT_BT_DARWIN) << "failed to create a device inquiry"; + } + + m_active = false; + } + + return self; +} + +- (void)dealloc +{ + // Noop if m_inquiry is nil. + [m_inquiry setDelegate:nil]; + if (m_active) + [m_inquiry stop]; + [m_inquiry release]; + + [super dealloc]; +} + +- (bool)isActive +{ + return m_active; +} + +- (IOReturn)start +{ + if (!m_inquiry) + return kIOReturnNoPower; + + if (m_active) + return kIOReturnBusy; + + m_active = true; + [m_inquiry clearFoundDevices]; + const IOReturn result = [m_inquiry start]; + if (result != kIOReturnSuccess) { + // QtBluetooth will probably convert an error into UnknownError, + // loosing the actual information. + qCWarning(QT_BT_DARWIN) << "failed with IOKit error code:" << result; + m_active = false; + } + + return result; +} + +- (IOReturn)stop +{ + if (m_active) { + Q_ASSERT_X(m_inquiry, Q_FUNC_INFO, "active but nil inquiry"); + + m_active = false; + const IOReturn res = [m_inquiry stop]; + if (res != kIOReturnSuccess) + m_active = true; + else + qCDebug(QT_BT_DARWIN) << "-stop, success (waiting for 'inquiryComplete')"; + + return res; + } + + return kIOReturnSuccess; +} + +- (void)deviceInquiryComplete:(IOBluetoothDeviceInquiry *)sender + error:(IOReturn)error aborted:(BOOL)aborted +{ + Q_UNUSED(aborted) + + if (sender != m_inquiry) // Can never happen in the current version. + return; + + m_active = false; + + Q_ASSERT_X(m_delegate, Q_FUNC_INFO, "invalid device inquiry delegate (null)"); + + if (error != kIOReturnSuccess) { + // QtBluetooth has not too many error codes, 'UnknownError' is not really + // useful, report the actual error code here: + qCWarning(QT_BT_DARWIN) << "IOKit error code: " << error; + m_delegate->error(error); + } else { + m_delegate->inquiryFinished(); + } +} + +- (void)deviceInquiryDeviceFound:(IOBluetoothDeviceInquiry *)sender + device:(IOBluetoothDevice *)device +{ + if (sender != m_inquiry) // Can never happen in the current version. + return; + + Q_ASSERT_X(m_delegate, Q_FUNC_INFO, "invalid device inquiry delegate (null)"); + m_delegate->classicDeviceFound(device); +} + +- (void)deviceInquiryStarted:(IOBluetoothDeviceInquiry *)sender +{ + Q_UNUSED(sender) +} + +@end diff --git a/src/bluetooth/darwin/btdeviceinquiry_p.h b/src/bluetooth/darwin/btdeviceinquiry_p.h new file mode 100644 index 00000000..fa4fd01c --- /dev/null +++ b/src/bluetooth/darwin/btdeviceinquiry_p.h @@ -0,0 +1,83 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#ifndef BTDEVICEINQUIRY_P_H +#define BTDEVICEINQUIRY_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "btdelegates_p.h" + +#include <QtCore/qglobal.h> + +#include <Foundation/Foundation.h> +#include <IOKit/IOReturn.h> + +#include <IOBluetooth/IOBluetooth.h> + +@interface QT_MANGLE_NAMESPACE(DarwinBTClassicDeviceInquiry) : NSObject<IOBluetoothDeviceInquiryDelegate> + +- (id)initWithDelegate:(QT_PREPEND_NAMESPACE(DarwinBluetooth::DeviceInquiryDelegate) *)delegate; +- (void)dealloc; + +- (bool)isActive; +- (IOReturn)start; +- (IOReturn)stop; + +//Obj-C delegate: +- (void)deviceInquiryComplete:(IOBluetoothDeviceInquiry *)sender + error:(IOReturn)error aborted:(BOOL)aborted; + +- (void)deviceInquiryDeviceFound:(IOBluetoothDeviceInquiry *)sender + device:(IOBluetoothDevice *)device; + +- (void)deviceInquiryStarted:(IOBluetoothDeviceInquiry *)sender; + +@end + +#endif diff --git a/src/bluetooth/darwin/btdevicepair.mm b/src/bluetooth/darwin/btdevicepair.mm new file mode 100644 index 00000000..2c212c2d --- /dev/null +++ b/src/bluetooth/darwin/btdevicepair.mm @@ -0,0 +1,215 @@ +/**************************************************************************** +** +** 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 "btdevicepair_p.h" +#include "btutility_p.h" + +#include <QtCore/qloggingcategory.h> +#include <QtCore/qdebug.h> + +QT_BEGIN_NAMESPACE + +namespace DarwinBluetooth { + +ObjCStrongReference<IOBluetoothDevice> device_with_address(const QBluetoothAddress &address) +{ + if (address.isNull()) + return ObjCStrongReference<IOBluetoothDevice>(nil, false); + + const BluetoothDeviceAddress &iobtAddress = iobluetooth_address(address); + ObjCStrongReference<IOBluetoothDevice> res([[IOBluetoothDevice deviceWithAddress:&iobtAddress] retain], false); + return res; +} + +} // namespace DarwinBluetooth + +QT_END_NAMESPACE + +QT_USE_NAMESPACE + +@implementation QT_MANGLE_NAMESPACE(DarwinBTClassicPairing) +{ + QT_PREPEND_NAMESPACE(QBluetoothAddress) m_targetAddress; + + bool m_active; + IOBluetoothDevicePair *m_pairing; // The real pairing request + QT_PREPEND_NAMESPACE(DarwinBluetooth)::PairingDelegate *m_object; +} + +- (id)initWithTarget:(const QBluetoothAddress &)address + delegate:(DarwinBluetooth::PairingDelegate *)object +{ + if (self = [super init]) { + Q_ASSERT_X(!address.isNull(), Q_FUNC_INFO, "invalid target address"); + Q_ASSERT_X(object, Q_FUNC_INFO, "invalid delegate (null)"); + + m_targetAddress = address; + m_object = object; + m_active = false; + } + + return self; +} + +- (void)dealloc +{ + [m_pairing stop]; + [m_pairing release]; + [super dealloc]; +} + +- (IOReturn) start +{ + if (m_active) + return kIOReturnBusy; + + Q_ASSERT_X(!m_targetAddress.isNull(), Q_FUNC_INFO, "invalid target address"); + + QT_BT_MAC_AUTORELEASEPOOL; + + const BluetoothDeviceAddress &iobtAddress = DarwinBluetooth::iobluetooth_address(m_targetAddress); + // Device is autoreleased. + IOBluetoothDevice *const device = [IOBluetoothDevice deviceWithAddress:&iobtAddress]; + if (!device) { + qCCritical(QT_BT_DARWIN) << "failed to create a device to pair with"; + return kIOReturnError; + } + + m_pairing = [[IOBluetoothDevicePair pairWithDevice:device] retain]; + if (!m_pairing) { + qCCritical(QT_BT_DARWIN) << "failed to create a device pair"; + return kIOReturnError; + } + + [m_pairing setDelegate:self]; + const IOReturn result = [m_pairing start]; + if (result != kIOReturnSuccess) { + [m_pairing release]; + m_pairing = nil; + } else + m_active = true; + + return result; +} + +- (bool)isActive +{ + return m_active; +} + +- (void)stop +{ + // stop: stops pairing, removes the delegate + // and disconnects if device was connected. + if (m_pairing) + [m_pairing stop]; +} + +- (const QBluetoothAddress &)targetAddress +{ + return m_targetAddress; +} + +- (IOBluetoothDevicePair *)pairingRequest +{ + return [[m_pairing retain] autorelease]; +} + +- (IOBluetoothDevice *)targetDevice +{ + return [m_pairing device];//It's retained/autoreleased by pair. +} + +// IOBluetoothDevicePairDelegate: + +- (void)devicePairingStarted:(id)sender +{ + Q_UNUSED(sender) +} + +- (void)devicePairingConnecting:(id)sender +{ + Q_UNUSED(sender) +} + +- (void)deviceParingPINCodeRequest:(id)sender +{ + Q_UNUSED(sender) +} + +- (void)devicePairingUserConfirmationRequest:(id)sender + numericValue:(BluetoothNumericValue)numericValue +{ + if (sender != m_pairing) // Can never happen. + return; + + Q_ASSERT_X(m_object, Q_FUNC_INFO, "invalid delegate (null)"); + + m_object->requestUserConfirmation(self, numericValue); +} + +- (void)devicePairingUserPasskeyNotification:(id)sender + passkey:(BluetoothPasskey)passkey +{ + Q_UNUSED(sender) + Q_UNUSED(passkey) +} + +- (void)devicePairingFinished:(id)sender error:(IOReturn)error +{ + Q_ASSERT_X(m_object, Q_FUNC_INFO, "invalid delegate (null)"); + + if (sender != m_pairing) // Can never happen though. + return; + + m_active = false; + if (error != kIOReturnSuccess) + m_object->error(self, error); + else + m_object->pairingFinished(self); +} + +- (void)deviceSimplePairingComplete:(id)sender + status:(BluetoothHCIEventStatus)status +{ + Q_UNUSED(sender) + Q_UNUSED(status) +} + +@end diff --git a/src/bluetooth/darwin/btdevicepair_p.h b/src/bluetooth/darwin/btdevicepair_p.h new file mode 100644 index 00000000..1b361258 --- /dev/null +++ b/src/bluetooth/darwin/btdevicepair_p.h @@ -0,0 +1,110 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#ifndef BTDEVICEPAIR_P_H +#define BTDEVICEPAIR_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qbluetoothaddress.h" +#include "btdelegates_p.h" +#include "btutility_p.h" + +#include <QtCore/qglobal.h> + +#include <Foundation/Foundation.h> + +#include <IOBluetooth/IOBluetooth.h> + +QT_BEGIN_NAMESPACE + +namespace DarwinBluetooth { + +ObjCStrongReference<IOBluetoothDevice> device_with_address(const QBluetoothAddress &address); + +} // Namespace DarwinBluetooth. + +QT_END_NAMESPACE + +@interface QT_MANGLE_NAMESPACE(DarwinBTClassicPairing) : NSObject<IOBluetoothDevicePairDelegate> + +- (id)initWithTarget:(const QBluetoothAddress &)address + delegate:(QT_PREPEND_NAMESPACE(DarwinBluetooth::PairingDelegate) *)object; + +- (void)dealloc; + +- (IOReturn)start; +- (bool)isActive; +- (void)stop; + +- (const QBluetoothAddress &)targetAddress; +- (IOBluetoothDevicePair *)pairingRequest; +- (IOBluetoothDevice *)targetDevice; + +// IOBluetoothDevicePairDelegate: + +- (void)devicePairingStarted:(id)sender; +- (void)devicePairingConnecting:(id)sender; +- (void)deviceParingPINCodeRequest:(id)sender; + +- (void)devicePairingUserConfirmationRequest:(id)sender + numericValue:(BluetoothNumericValue)numericValue; + +- (void)devicePairingUserPasskeyNotification:(id)sender + passkey:(BluetoothPasskey)passkey; + +- (void)devicePairingFinished:(id)sender + error:(IOReturn)error; + +- (void)deviceSimplePairingComplete:(id)sender + status:(BluetoothHCIEventStatus)status; + +@end + + +#endif diff --git a/src/bluetooth/darwin/btgcdtimer.mm b/src/bluetooth/darwin/btgcdtimer.mm new file mode 100644 index 00000000..9105a8fb --- /dev/null +++ b/src/bluetooth/darwin/btgcdtimer.mm @@ -0,0 +1,146 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtBluetooth module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "btgcdtimer_p.h" +#include "btutility_p.h" + +#include <QtCore/qdebug.h> + +#include <algorithm> + +QT_USE_NAMESPACE +using namespace DarwinBluetooth; + +@implementation QT_MANGLE_NAMESPACE(DarwinBTGCDTimer) { +@private + qint64 timeoutMS; + qint64 timeoutStepMS; + + // Optional: + id objectUnderWatch; + OperationTimeout timeoutType; + + QElapsedTimer timer; + id<QT_MANGLE_NAMESPACE(GCDTimerDelegate)> timeoutHandler; + + bool cancelled; +} + +- (instancetype)initWithDelegate:(id<QT_MANGLE_NAMESPACE(GCDTimerDelegate)>)delegate +{ + if (self = [super init]) { + timeoutHandler = delegate; + timeoutMS = 0; + timeoutStepMS = 0; + objectUnderWatch = nil; + timeoutType = OperationTimeout::none; + cancelled = false; + } + return self; +} + +- (void)watchAfter:(id)object withTimeoutType:(OperationTimeout)type +{ + objectUnderWatch = object; + timeoutType = type; +} + +- (void)startWithTimeout:(qint64)ms step:(qint64)stepMS +{ + Q_ASSERT(!timeoutMS && !timeoutStepMS); + Q_ASSERT(!cancelled); + + if (!timeoutHandler) { + // Nobody to report timeout to, no need to start any task then. + return; + } + + if (ms <= 0 || stepMS <= 0) { + qCWarning(QT_BT_DARWIN, "Invalid timeout/step parameters"); + return; + } + + timeoutMS = ms; + timeoutStepMS = stepMS; + timer.start(); + + [self handleTimeout]; +} + +- (void)handleTimeout +{ + if (cancelled) + return; + + const qint64 elapsed = timer.elapsed(); + if (elapsed >= timeoutMS) { + [timeoutHandler timeout:self]; + } else { + // Re-schedule: + dispatch_queue_t leQueue(qt_LE_queue()); + Q_ASSERT(leQueue); + const qint64 timeChunkMS = std::min(timeoutMS - elapsed, timeoutStepMS); + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, + int64_t(timeChunkMS / 1000. * NSEC_PER_SEC)), + leQueue, + ^{ + [self handleTimeout]; + }); + } +} + +- (void)cancelTimer +{ + cancelled = true; + timeoutHandler = nil; + objectUnderWatch = nil; + timeoutType = OperationTimeout::none; +} + +- (id)objectUnderWatch +{ + return objectUnderWatch; +} + +- (OperationTimeout)timeoutType +{ + return timeoutType; +} + +@end diff --git a/src/bluetooth/darwin/btgcdtimer_p.h b/src/bluetooth/darwin/btgcdtimer_p.h new file mode 100644 index 00000000..dd67d6e8 --- /dev/null +++ b/src/bluetooth/darwin/btgcdtimer_p.h @@ -0,0 +1,108 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtBluetooth module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef BTGCDTIMER_P_H +#define BTGCDTIMER_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "btutility_p.h" + +#include <QtCore/qelapsedtimer.h> +#include <QtCore/qglobal.h> + +#include <Foundation/Foundation.h> + +QT_BEGIN_NAMESPACE + +namespace DarwinBluetooth { + +enum class OperationTimeout +{ + none, + serviceDiscovery, + includedServicesDiscovery, + characteristicsDiscovery, + characteristicRead, + descriptorsDiscovery, + descriptorRead, + characteristicWrite +}; + +} // namespace DarwinBluetooth + +QT_END_NAMESPACE + +@protocol QT_MANGLE_NAMESPACE(GCDTimerDelegate) +@required +- (void)timeout:(id)sender; +@end + +@interface QT_MANGLE_NAMESPACE(DarwinBTGCDTimer) : NSObject +- (instancetype)initWithDelegate:(id<QT_MANGLE_NAMESPACE(GCDTimerDelegate)>)delegate; +- (void)watchAfter:(id)object withTimeoutType:(QT_PREPEND_NAMESPACE(DarwinBluetooth)::OperationTimeout)type; +- (void)startWithTimeout:(qint64)ms step:(qint64)stepMS; +- (void)handleTimeout; +- (void)cancelTimer; +- (id)objectUnderWatch; +- (QT_PREPEND_NAMESPACE(DarwinBluetooth)::OperationTimeout)timeoutType; +@end + +QT_BEGIN_NAMESPACE + +namespace DarwinBluetooth { + +using GCDTimerObjC = QT_MANGLE_NAMESPACE(DarwinBTGCDTimer); +using GCDTimer = ObjCStrongReference<GCDTimerObjC>; + +} // namespace DarwinBluetooth + +QT_END_NAMESPACE + +#endif // BTGCDTIMER_P_H + diff --git a/src/bluetooth/darwin/btl2capchannel.mm b/src/bluetooth/darwin/btl2capchannel.mm new file mode 100644 index 00000000..e440a0ee --- /dev/null +++ b/src/bluetooth/darwin/btl2capchannel.mm @@ -0,0 +1,258 @@ +/**************************************************************************** +** +** 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 "btl2capchannel_p.h" +#include "qbluetoothaddress.h" +#include "btdelegates_p.h" +#include "btutility_p.h" + +#include <QtCore/qloggingcategory.h> +#include <QtCore/qdebug.h> + +QT_USE_NAMESPACE + +@implementation QT_MANGLE_NAMESPACE(DarwinBTL2CAPChannel) +{ + QT_PREPEND_NAMESPACE(DarwinBluetooth)::ChannelDelegate *delegate; + IOBluetoothDevice *device; + IOBluetoothL2CAPChannel *channel; + bool connected; +} + +- (id)initWithDelegate:(DarwinBluetooth::ChannelDelegate *)aDelegate +{ + Q_ASSERT_X(aDelegate, Q_FUNC_INFO, "invalid delegate (null)"); + + if (self = [super init]) { + delegate = aDelegate; + device = nil; + channel = nil; + connected = false; + } + + return self; +} + +- (id)initWithDelegate:(QT_PREPEND_NAMESPACE(DarwinBluetooth::ChannelDelegate) *)aDelegate + channel:(IOBluetoothL2CAPChannel *)aChannel +{ + // This type of channel does not require connect, it's created with + // already open channel. + Q_ASSERT_X(aDelegate, Q_FUNC_INFO, "invalid delegate (null)"); + Q_ASSERT_X(channel, Q_FUNC_INFO, "invalid channel (nil)"); + + if (self = [super init]) { + delegate = aDelegate; + channel = [aChannel retain]; + [channel setDelegate:self]; + device = [channel.device retain]; + connected = true; + } + + return self; +} + +- (void)dealloc +{ + // TODO: test if this implementation works at all! + if (channel) { + [channel setDelegate:nil]; + // From Apple's docs: + // "This method may only be called by the client that opened the channel + // in the first place. In the future asynchronous and synchronous versions + // will be provided that let the client know when the close process has been finished." + [channel closeChannel]; + [channel release]; + } + + [device release]; + + [super dealloc]; +} + +- (IOReturn)connectAsyncToDevice:(const QBluetoothAddress &)address + withPSM:(BluetoothL2CAPChannelID)psm +{ + if (address.isNull()) { + qCCritical(QT_BT_DARWIN) << "invalid peer address"; + return kIOReturnNoDevice; + } + + // Can never be called twice. + if (connected || device || channel) { + qCCritical(QT_BT_DARWIN) << "connection is already active"; + return kIOReturnStillOpen; + } + + QT_BT_MAC_AUTORELEASEPOOL; + + const BluetoothDeviceAddress iobtAddress = DarwinBluetooth::iobluetooth_address(address); + device = [IOBluetoothDevice deviceWithAddress:&iobtAddress]; + if (!device) { + qCCritical(QT_BT_DARWIN) << "failed to create a device"; + return kIOReturnNoDevice; + } + + const IOReturn status = [device openL2CAPChannelAsync:&channel withPSM:psm delegate:self]; + if (status != kIOReturnSuccess) { + qCCritical(QT_BT_DARWIN) << "failed to open L2CAP channel"; + // device is still autoreleased. + device = nil; + return status; + } + + [channel retain];// What if we're closed already? + [device retain]; + + return kIOReturnSuccess; +} + +// IOBluetoothL2CAPChannelDelegate: + +- (void)l2capChannelData:(IOBluetoothL2CAPChannel*)l2capChannel + data:(void *)dataPointer length:(size_t)dataLength +{ + Q_UNUSED(l2capChannel) + + Q_ASSERT_X(delegate, Q_FUNC_INFO, "invalid delegate (null)"); + + if (dataPointer && dataLength) + delegate->readChannelData(dataPointer, dataLength); +} + +- (void)l2capChannelOpenComplete:(IOBluetoothL2CAPChannel*) + l2capChannel status:(IOReturn)error +{ + Q_UNUSED(l2capChannel) + + Q_ASSERT_X(delegate, Q_FUNC_INFO, "invalid delegate (null)"); + + if (error != kIOReturnSuccess) { + delegate->setChannelError(error); + } else { + connected = true; + delegate->channelOpenComplete(); + } +} + +- (void)l2capChannelClosed:(IOBluetoothL2CAPChannel*)l2capChannel +{ + Q_UNUSED(l2capChannel) + + Q_ASSERT_X(delegate, Q_FUNC_INFO, "invalid delegate (null)"); + delegate->channelClosed(); + connected = false; +} + +- (void)l2capChannelReconfigured:(IOBluetoothL2CAPChannel*)l2capChannel +{ + Q_UNUSED(l2capChannel) +} + +- (void)l2capChannelWriteComplete:(IOBluetoothL2CAPChannel*)l2capChannel + refcon:(void*)refcon status:(IOReturn)error +{ + Q_UNUSED(l2capChannel) + Q_UNUSED(refcon) + + Q_ASSERT_X(delegate, Q_FUNC_INFO, "invalid delegate (null)"); + + if (error != kIOReturnSuccess) + delegate->setChannelError(error); + else + delegate->writeComplete(); +} + +- (void)l2capChannelQueueSpaceAvailable:(IOBluetoothL2CAPChannel*)l2capChannel +{ + Q_UNUSED(l2capChannel) +} + +// Aux. methods. +- (BluetoothL2CAPPSM)getPSM +{ + if (channel) + return channel.PSM; + + return 0; +} + +- (BluetoothDeviceAddress)peerAddress +{ + const BluetoothDeviceAddress *const addr = device ? [device getAddress] + : nullptr; + if (addr) + return *addr; + + return BluetoothDeviceAddress(); +} + +- (NSString *)peerName +{ + if (device) + return device.name; + + return nil; +} + +- (bool)isConnected +{ + return connected; +} + +- (IOReturn) writeSync:(void*)data length:(UInt16)length +{ + Q_ASSERT_X(data, Q_FUNC_INFO, "invalid data (null)"); + Q_ASSERT_X(length, Q_FUNC_INFO, "invalid data size"); + Q_ASSERT_X(connected && channel, Q_FUNC_INFO, "invalid L2CAP channel"); + + return [channel writeSync:data length:length]; +} + +- (IOReturn) writeAsync:(void*)data length:(UInt16)length +{ + Q_ASSERT_X(data, Q_FUNC_INFO, "invalid data (null)"); + Q_ASSERT_X(length, Q_FUNC_INFO, "invalid data size"); + Q_ASSERT_X(connected && channel, Q_FUNC_INFO, "invalid L2CAP channel"); + + return [channel writeAsync:data length:length refcon:nullptr]; +} + + +@end diff --git a/src/bluetooth/darwin/btl2capchannel_p.h b/src/bluetooth/darwin/btl2capchannel_p.h new file mode 100644 index 00000000..32122fe8 --- /dev/null +++ b/src/bluetooth/darwin/btl2capchannel_p.h @@ -0,0 +1,118 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#ifndef BTL2CAPCHANNEL_P_H +#define BTL2CAPCHANNEL_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/qglobal.h> + +#include <Foundation/Foundation.h> + +#include <IOBluetooth/IOBluetooth.h> + +#include <cstddef> + +QT_BEGIN_NAMESPACE + +class QBluetoothAddress; + +namespace DarwinBluetooth { + +class ChannelDelegate; + +} + +QT_END_NAMESPACE + +@interface QT_MANGLE_NAMESPACE(DarwinBTL2CAPChannel) : NSObject<IOBluetoothL2CAPChannelDelegate> + +- (id)initWithDelegate:(QT_PREPEND_NAMESPACE(DarwinBluetooth)::ChannelDelegate *)aDelegate; +- (id)initWithDelegate:(QT_PREPEND_NAMESPACE(DarwinBluetooth)::ChannelDelegate *)aDelegate + channel:(IOBluetoothL2CAPChannel *)aChannel; + +- (void)dealloc; + +// Async. connection (connect can be called only once). +- (IOReturn)connectAsyncToDevice:(const QT_PREPEND_NAMESPACE(QBluetoothAddress) &)address + withPSM:(BluetoothL2CAPChannelID)psm; + +// IOBluetoothL2CAPChannelDelegate: +- (void)l2capChannelData:(IOBluetoothL2CAPChannel*)l2capChannel + data:(void *)dataPointer length:(size_t)dataLength; +- (void)l2capChannelOpenComplete:(IOBluetoothL2CAPChannel*) + l2capChannel status:(IOReturn)error; +- (void)l2capChannelClosed:(IOBluetoothL2CAPChannel*)l2capChannel; +- (void)l2capChannelReconfigured:(IOBluetoothL2CAPChannel*)l2capChannel; +- (void)l2capChannelWriteComplete:(IOBluetoothL2CAPChannel*)l2capChannel + refcon:(void*)refcon status:(IOReturn)error; +- (void)l2capChannelQueueSpaceAvailable:(IOBluetoothL2CAPChannel*)l2capChannel; + +// +- (BluetoothL2CAPPSM)getPSM; +- (BluetoothDeviceAddress)peerAddress; +- (NSString *)peerName; +- (bool)isConnected; + +// Writes the given data synchronously over the target L2CAP channel to the remote +// device. +// The length of the data may not exceed the L2CAP channel's outgoing MTU. +// This method will block until the data has been successfully sent to the +// hardware for transmission (or an error occurs). +- (IOReturn) writeSync:(void*)data length:(UInt16)length; + +// The length of the data may not exceed the L2CAP channel's outgoing MTU. +// When the data has been successfully passed to the hardware to be transmitted, +// the delegate method -l2capChannelWriteComplete:refcon:status: will be called. +// Returns kIOReturnSuccess if the data was buffered successfully. +- (IOReturn) writeAsync:(void*)data length:(UInt16)length; + +@end + +#endif diff --git a/src/bluetooth/darwin/btledeviceinquiry.mm b/src/bluetooth/darwin/btledeviceinquiry.mm new file mode 100644 index 00000000..b3f29035 --- /dev/null +++ b/src/bluetooth/darwin/btledeviceinquiry.mm @@ -0,0 +1,378 @@ +/**************************************************************************** +** +** 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 "qbluetoothdeviceinfo.h" +#include "btledeviceinquiry_p.h" +#include "qbluetoothuuid.h" +#include "btnotifier_p.h" +#include "btutility_p.h" + +#include <QtCore/qloggingcategory.h> +#include <QtCore/qdebug.h> +#include <QtCore/qendian.h> + +#include <algorithm> + +QT_BEGIN_NAMESPACE + +namespace DarwinBluetooth { + +QBluetoothUuid qt_uuid(NSUUID *nsUuid) +{ + if (!nsUuid) + return QBluetoothUuid(); + + uuid_t uuidData = {}; + [nsUuid getUUIDBytes:uuidData]; + quint128 qtUuidData = {}; + std::copy(uuidData, uuidData + 16, qtUuidData.data); + return QBluetoothUuid(qtUuidData); +} + +const int timeStepMS = 100; +const int powerOffTimeoutMS = 30000; + +struct AdvertisementData { + // That's what CoreBluetooth has: + // CBAdvertisementDataLocalNameKey + // CBAdvertisementDataTxPowerLevelKey + // CBAdvertisementDataServiceUUIDsKey + // CBAdvertisementDataServiceDataKey + // CBAdvertisementDataManufacturerDataKey + // CBAdvertisementDataOverflowServiceUUIDsKey + // CBAdvertisementDataIsConnectable + // CBAdvertisementDataSolicitedServiceUUIDsKey + + // For now, we "parse": + QString localName; + QVector<QBluetoothUuid> serviceUuids; + QHash<quint16, QByteArray> manufacturerData; + // TODO: other keys probably? + AdvertisementData(NSDictionary *AdvertisementData); +}; + +AdvertisementData::AdvertisementData(NSDictionary *advertisementData) +{ + if (!advertisementData) + return; + + // ... constant CBAdvertisementDataLocalNameKey ... + // NSString containing the local name of a peripheral. + NSObject *value = [advertisementData objectForKey:CBAdvertisementDataLocalNameKey]; + if (value && [value isKindOfClass:[NSString class]]) + localName = QString::fromNSString(static_cast<NSString *>(value)); + + // ... constant CBAdvertisementDataServiceUUIDsKey ... + // A list of one or more CBUUID objects, representing CBService UUIDs. + + value = [advertisementData objectForKey:CBAdvertisementDataServiceUUIDsKey]; + if (value && [value isKindOfClass:[NSArray class]]) { + NSArray *uuids = static_cast<NSArray *>(value); + for (CBUUID *cbUuid in uuids) + serviceUuids << qt_uuid(cbUuid); + } + + value = [advertisementData objectForKey:CBAdvertisementDataManufacturerDataKey]; + if (value && [value isKindOfClass:[NSData class]]) { + QByteArray data = QByteArray::fromNSData(static_cast<NSData *>(value)); + manufacturerData.insert(qFromLittleEndian<quint16>(data.constData()), data.mid(2)); + } +} + +} + +QT_END_NAMESPACE + +QT_USE_NAMESPACE + +@interface QT_MANGLE_NAMESPACE(DarwinBTLEDeviceInquiry)(PrivateAPI) +- (void)stopScanSafe; +- (void)stopNotifier; +@end + +@implementation QT_MANGLE_NAMESPACE(DarwinBTLEDeviceInquiry) +{ + LECBManagerNotifier *notifier; + ObjCScopedPointer<CBCentralManager> manager; + + QList<QBluetoothDeviceInfo> devices; + LEInquiryState internalState; + int inquiryTimeoutMS; + + QT_PREPEND_NAMESPACE(DarwinBluetooth)::GCDTimer elapsedTimer; +} + +-(id)initWithNotifier:(LECBManagerNotifier *)aNotifier +{ + if (self = [super init]) { + Q_ASSERT(aNotifier); + notifier = aNotifier; + internalState = InquiryStarting; + inquiryTimeoutMS = DarwinBluetooth::defaultLEScanTimeoutMS; + } + + return self; +} + +- (void)dealloc +{ + [self stopScanSafe]; + [manager setDelegate:nil]; + [elapsedTimer cancelTimer]; + [self stopNotifier]; + [super dealloc]; +} + +- (void)timeout:(id)sender +{ + Q_UNUSED(sender) + + if (internalState == InquiryActive) { + [self stopScanSafe]; + [manager setDelegate:nil]; + internalState = InquiryFinished; + Q_ASSERT(notifier); + emit notifier->discoveryFinished(); + } else if (internalState == InquiryStarting) { + // This is interesting on iOS only, where the system shows an alert + // asking to enable Bluetooth in the 'Settings' app. If not done yet + // (after 30 seconds) - we consider this as an error. + [manager setDelegate:nil]; + internalState = ErrorPoweredOff; + Q_ASSERT(notifier); + emit notifier->CBManagerError(QBluetoothDeviceDiscoveryAgent::PoweredOffError); + } +} + +- (void)startWithTimeout:(int)timeout +{ + dispatch_queue_t leQueue(DarwinBluetooth::qt_LE_queue()); + Q_ASSERT(leQueue); + inquiryTimeoutMS = timeout; + manager.reset([[CBCentralManager alloc] initWithDelegate:self queue:leQueue]); +} + +- (void)centralManagerDidUpdateState:(CBCentralManager *)central +{ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunguarded-availability-new" + + if (central != manager) + return; + + if (internalState != InquiryActive && internalState != InquiryStarting) + return; + + Q_ASSERT(notifier); + + using namespace DarwinBluetooth; + + const auto state = central.state; +#if QT_IOS_PLATFORM_SDK_EQUAL_OR_ABOVE(__IPHONE_10_0) || QT_OSX_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_13) + if (state == CBManagerStatePoweredOn) { +#else + if (state == CBCentralManagerStatePoweredOn) { +#endif + if (internalState == InquiryStarting) { + internalState = InquiryActive; + + if (inquiryTimeoutMS > 0) { + [elapsedTimer cancelTimer]; + elapsedTimer.resetWithoutRetain([[GCDTimerObjC alloc] initWithDelegate:self]); + [elapsedTimer startWithTimeout:inquiryTimeoutMS step:timeStepMS]; + } + + [manager scanForPeripheralsWithServices:nil options:nil]; + } // Else we ignore. +#if QT_IOS_PLATFORM_SDK_EQUAL_OR_ABOVE(__IPHONE_10_0) || QT_OSX_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_13) + } else if (state == CBManagerStateUnsupported || state == CBManagerStateUnauthorized) { +#else + } else if (state == CBCentralManagerStateUnsupported || state == CBCentralManagerStateUnauthorized) { +#endif + if (internalState == InquiryActive) { + [self stopScanSafe]; + // Not sure how this is possible at all, + // probably, can never happen. + internalState = ErrorPoweredOff; + emit notifier->CBManagerError(QBluetoothDeviceDiscoveryAgent::PoweredOffError); + } else { + internalState = ErrorLENotSupported; + emit notifier->LEnotSupported(); + } + + [manager setDelegate:nil]; +#if QT_IOS_PLATFORM_SDK_EQUAL_OR_ABOVE(__IPHONE_10_0) || QT_OSX_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_13) + } else if (state == CBManagerStatePoweredOff) { +#else + } else if (state == CBCentralManagerStatePoweredOff) { +#endif + +#ifndef Q_OS_MACOS + if (internalState == InquiryStarting) { + // On iOS a user can see at this point an alert asking to + // enable Bluetooth in the "Settings" app. If a user does so, + // we'll receive 'PoweredOn' state update later. + // No change in internalState. Wait for 30 seconds. + [elapsedTimer cancelTimer]; + elapsedTimer.resetWithoutRetain([[GCDTimerObjC alloc] initWithDelegate:self]); + [elapsedTimer startWithTimeout:powerOffTimeoutMS step:300]; + return; + } +#else + Q_UNUSED(powerOffTimeoutMS) +#endif // Q_OS_MACOS + [elapsedTimer cancelTimer]; + [self stopScanSafe]; + [manager setDelegate:nil]; + internalState = ErrorPoweredOff; + // On macOS we report PoweredOffError and our C++ owner will delete us + // (here we're kwnon as 'self'). Connection is Qt::QueuedConnection so we + // are apparently safe to call -stopNotifier after the signal. + emit notifier->CBManagerError(QBluetoothDeviceDiscoveryAgent::PoweredOffError); + [self stopNotifier]; + } else { + // The following two states we ignore (from Apple's docs): + //" + // -CBCentralManagerStateUnknown + // The current state of the central manager is unknown; + // an update is imminent. + // + // -CBCentralManagerStateResetting + // The connection with the system service was momentarily + // lost; an update is imminent. " + // Wait for this imminent update. + } + +#pragma clang diagnostic pop +} + +- (void)stopScanSafe +{ + // CoreBluetooth warns about API misused if we call stopScan in a state + // other than powered on. Hence this 'Safe' ... + if (!manager) + return; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunguarded-availability-new" + + if (internalState == InquiryActive) { + const auto state = manager.data().state; + #if QT_IOS_PLATFORM_SDK_EQUAL_OR_ABOVE(__IPHONE_10_0) || QT_OSX_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_13) + if (state == CBManagerStatePoweredOn) + #else + if (state == CBCentralManagerStatePoweredOn) + #endif + [manager stopScan]; + } + +#pragma clang diagnostic pop +} + +- (void)stopNotifier +{ + if (notifier) { + notifier->disconnect(); + notifier->deleteLater(); + notifier = nullptr; + } +} + +- (void)stop +{ + [self stopScanSafe]; + [manager setDelegate:nil]; + [elapsedTimer cancelTimer]; + [self stopNotifier]; + internalState = InquiryCancelled; +} + +- (void)centralManager:(CBCentralManager *)central + didDiscoverPeripheral:(CBPeripheral *)peripheral + advertisementData:(NSDictionary *)advertisementData + RSSI:(NSNumber *)RSSI +{ + using namespace DarwinBluetooth; + + if (central != manager) + return; + + if (internalState != InquiryActive) + return; + + if (!notifier) + return; + + QBluetoothUuid deviceUuid; + + if (!peripheral.identifier) { + qCWarning(QT_BT_DARWIN) << "peripheral without NSUUID"; + return; + } + + deviceUuid = DarwinBluetooth::qt_uuid(peripheral.identifier); + + if (deviceUuid.isNull()) { + qCWarning(QT_BT_DARWIN) << "no way to address peripheral, QBluetoothUuid is null"; + return; + } + + const AdvertisementData qtAdvData(advertisementData); + QString name(qtAdvData.localName); + if (!name.size() && peripheral.name) + name = QString::fromNSString(peripheral.name); + + // TODO: fix 'classOfDevice' (0 for now). + QBluetoothDeviceInfo newDeviceInfo(deviceUuid, name, 0); + if (RSSI) + newDeviceInfo.setRssi([RSSI shortValue]); + + if (qtAdvData.serviceUuids.size()) + newDeviceInfo.setServiceUuids(qtAdvData.serviceUuids); + + const QList<quint16> keys = qtAdvData.manufacturerData.keys(); + for (quint16 key : keys) + newDeviceInfo.setManufacturerData(key, qtAdvData.manufacturerData.value(key)); + + // CoreBluetooth scans only for LE devices. + newDeviceInfo.setCoreConfigurations(QBluetoothDeviceInfo::LowEnergyCoreConfiguration); + emit notifier->deviceDiscovered(newDeviceInfo); +} + +@end diff --git a/src/bluetooth/darwin/btledeviceinquiry_p.h b/src/bluetooth/darwin/btledeviceinquiry_p.h new file mode 100644 index 00000000..58c66e56 --- /dev/null +++ b/src/bluetooth/darwin/btledeviceinquiry_p.h @@ -0,0 +1,103 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#ifndef BTLEDEVICEINQUIRY_P_H +#define BTLEDEVICEINQUIRY_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qbluetoothdevicediscoveryagent.h" +#include "qbluetoothdeviceinfo.h" +#include "btgcdtimer_p.h" +#include "btutility_p.h" + +#include <QtCore/qglobal.h> +#include <QtCore/qlist.h> + +#include <Foundation/Foundation.h> + +#include <CoreBluetooth/CoreBluetooth.h> + +QT_BEGIN_NAMESPACE + +class QBluetoothUuid; + +namespace DarwinBluetooth +{ + +class LECBManagerNotifier; + +} // namespace DarwinBluetooth + +QT_END_NAMESPACE + +using QT_PREPEND_NAMESPACE(DarwinBluetooth)::LECBManagerNotifier; +using QT_PREPEND_NAMESPACE(DarwinBluetooth)::ObjCScopedPointer; + +enum LEInquiryState +{ + InquiryStarting, + InquiryActive, + InquiryFinished, + InquiryCancelled, + ErrorPoweredOff, + ErrorLENotSupported +}; + +@interface QT_MANGLE_NAMESPACE(DarwinBTLEDeviceInquiry) : NSObject<CBCentralManagerDelegate, QT_MANGLE_NAMESPACE(GCDTimerDelegate)> +- (id)initWithNotifier:(LECBManagerNotifier *)aNotifier; +- (void)dealloc; + +// IMPORTANT: both 'startWithTimeout' and 'stop' MUST be executed on the "Qt's +// LE queue". +- (void)startWithTimeout:(int)timeout; +- (void)stop; + +@end + +#endif diff --git a/src/bluetooth/darwin/btnotifier.cpp b/src/bluetooth/darwin/btnotifier.cpp new file mode 100644 index 00000000..24f54a09 --- /dev/null +++ b/src/bluetooth/darwin/btnotifier.cpp @@ -0,0 +1 @@ +#include "btnotifier_p.h" diff --git a/src/bluetooth/darwin/btnotifier_p.h b/src/bluetooth/darwin/btnotifier_p.h new file mode 100644 index 00000000..e074a225 --- /dev/null +++ b/src/bluetooth/darwin/btnotifier_p.h @@ -0,0 +1,105 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#ifndef BTNOTIFIER_P_H +#define BTNOTIFIER_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + + +#include "qbluetoothdevicediscoveryagent.h" +#include "qlowenergycontroller.h" +#include "qbluetoothdeviceinfo.h" +#include "qbluetoothuuid.h" +#include "qbluetooth.h" + +#include <QtCore/qsharedpointer.h> +#include <QtCore/qbytearray.h> +#include <QtCore/qglobal.h> +#include <QtCore/qobject.h> + +QT_BEGIN_NAMESPACE + +class QLowEnergyServicePrivate; + +namespace DarwinBluetooth +{ + +class LECBManagerNotifier : public QObject +{ + Q_OBJECT + +Q_SIGNALS: + void deviceDiscovered(QBluetoothDeviceInfo deviceInfo); + void discoveryFinished(); + + void connected(); + void disconnected(); + + void serviceDiscoveryFinished(); + void serviceDetailsDiscoveryFinished(QSharedPointer<QLowEnergyServicePrivate> service); + void characteristicRead(QLowEnergyHandle charHandle, const QByteArray &value); + void characteristicWritten(QLowEnergyHandle charHandle, const QByteArray &value); + void characteristicUpdated(QLowEnergyHandle charHandle, const QByteArray &value); + void descriptorRead(QLowEnergyHandle descHandle, const QByteArray &value); + void descriptorWritten(QLowEnergyHandle descHandle, const QByteArray &value); + void notificationEnabled(QLowEnergyHandle charHandle, bool enabled); + void servicesWereModified(); + + void LEnotSupported(); + void CBManagerError(QBluetoothDeviceDiscoveryAgent::Error error); + void CBManagerError(QLowEnergyController::Error error); + void CBManagerError(const QBluetoothUuid &serviceUuid, QLowEnergyController::Error error); + void CBManagerError(const QBluetoothUuid &serviceUuid, QLowEnergyService::ServiceError error); +}; + +} // namespace DarwinBluetooth + +QT_END_NAMESPACE + +#endif diff --git a/src/bluetooth/darwin/btobexsession.mm b/src/bluetooth/darwin/btobexsession.mm new file mode 100644 index 00000000..ba48aab0 --- /dev/null +++ b/src/bluetooth/darwin/btobexsession.mm @@ -0,0 +1,841 @@ +/**************************************************************************** +** +** 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 "qbluetoothaddress.h" +#include "btobexsession_p.h" +#include "btutility_p.h" + +#include <QtCore/qloggingcategory.h> +#include <QtCore/qvector.h> +#include <QtCore/qdebug.h> +#include <QtCore/qlist.h> + +#include <algorithm> +#include <cstddef> +#include <limits> + +QT_BEGIN_NAMESPACE + +namespace DarwinBluetooth +{ + +OBEXSessionDelegate::~OBEXSessionDelegate() +{ +} + +namespace { + +struct OBEXHeader +{ + OBEXHeader() : headerID(0) + { + } + + quint8 headerID; + QVariant value; +}; + +enum { + // Bits 7 and 8 == header's format. + OBEXHeaderFormatMask = 0xc0, + // + OBEXHeaderFormatUnicode = 0, // 87 + OBEXHeaderFormatByteSequence = 0x40, // 0100 0000 + OBEXHeaderFormat1Byte = 0x80, // 1000 0000 + OBEXHeaderFormat4Byte = 0xc0, // 1100 0000 + +}; + +quint32 extract_uint32(const uint8_t *bytes) +{ + // Four byte value, high byte first. + Q_ASSERT_X(bytes, Q_FUNC_INFO, "invalid input data (null)"); + + uint32_t value = uint32_t(); + std::copy(bytes, bytes + sizeof value, reinterpret_cast<uint8_t *>(&value)); + + return NSSwapBigIntToHost(value); +} + +quint16 extract_uint16(const uint8_t *bytes) +{ + // Two byte value, high byte first. + Q_ASSERT_X(bytes, Q_FUNC_INFO, "invalid input data (null)"); + + uint16_t value = uint16_t(); + std::copy(bytes, bytes + sizeof value, reinterpret_cast<uint8_t *>(&value)); + + return NSSwapBigShortToHost(value); +} + +QString extract_qstring(const uint8_t *bytes, quint16 stringLength) +{ + if (bytes && stringLength) { + NSString * const nsString = [[NSString alloc] initWithBytes:bytes + length:stringLength + encoding:NSUnicodeStringEncoding]; + if (nsString) + return QString::fromNSString(nsString); + } + + // Empty string is an error, "valid" empty strings are + // handled separately. + return QString(); +} + +QList<OBEXHeader> qt_bluetooth_headers(const uint8_t *data, std::size_t length) +{ + // Convert a data from IOBluetooth into something, Qt understands. + // Possible formats (bits 7 and 8): + // 1. 00 Two bytes of length folowed by a null-terminated + // Unicode text string (length is unsigned integer; + // it covers the header ID and the whole of the header + // value, including the length bytes and the two bytes + // of null terminator. + // 2. 01 Two bytes of length followed by a byte sequence (length + // is an unsigned integer sent high byte first; it covers + // the header ID and the whole of the header value). + // 3. 10 A single byte value. + // 4. 11 A four byte value, sent high byte first. + + Q_ASSERT_X(data, Q_FUNC_INFO, "invalid data (null)"); + Q_ASSERT_X(length >= 2, Q_FUNC_INFO, "invalid data length"); + + Q_UNUSED(data) + Q_UNUSED(length) + + QList<OBEXHeader> empty; + QList<OBEXHeader> qtHeaders; + + for (std::size_t i = 0; i < length;) { + std::size_t headerLength = 0; + OBEXHeader header; + header.headerID = data[i]; + + switch (data[i] & OBEXHeaderFormatMask) { + case OBEXHeaderFormatUnicode: + { + if (i + 3 > length) + return empty; + headerLength = extract_uint16(data + i + 1); + // Invalid length or input data: + if (headerLength < 3 || i + headerLength > length) + return empty; + if (headerLength == 3 || headerLength == 5) { // Can 5 ever happen? + header.value.fromValue<QString>(QString()); + } else if (headerLength > 5) {// TODO: We do not check now, that the string actually valid. + const QString value(extract_qstring(data + i + 3, headerLength - 5)); + if (!value.length()) // Some error? + return empty; + header.value.setValue<QString>(value); + } else // Still something weird. + return empty; + break; + } + case OBEXHeaderFormatByteSequence: + { + if (i + 3 > length) + return empty; + headerLength = extract_uint16(data + i + 1); + // Something is wrong: + if (headerLength < 3 || i + headerLength > length) + return empty; + QVector<unsigned char> value; + if (headerLength > 3) { + value.resize(headerLength - 3); + std::copy(data, data + headerLength, value.begin()); + } + header.value.setValue<QVector<unsigned char> >(value); + break; + } + case OBEXHeaderFormat1Byte: + { + // 1 byte integer + 1 byte headerID == 2 + if (i + 2 > length) + return empty; + headerLength = 2; + header.value.setValue<quint8>(data[i + 1]); + break; + } + case OBEXHeaderFormat4Byte: + { + // 4 byte integer + 1 byte headerID == 5 + if (i + 5 > length) + return empty; + headerLength = 5; + header.value.setValue<quint32>(extract_uint32(data + i + 1)); + break; + } + default: + qCWarning(QT_BT_DARWIN) << "invalid header format"; + return empty; + } + + i += headerLength; + qtHeaders.push_back(header); + } + + return qtHeaders; +} + +bool append_uint16(ObjCStrongReference<NSMutableData> headers, uint16_t value) +{ + if (!headers) + return false; + + const NSUInteger length = [headers length]; + const uint16_t valueSwapped = NSSwapHostShortToBig(value); + [headers appendBytes:&valueSwapped length:sizeof valueSwapped]; + + return [headers length] - length == 2; +} + + +bool append_four_byte_header(ObjCStrongReference<NSMutableData> headers, uint8_t headerID, + uint32_t headerValue) +{ + if (!headers) + return false; + + const NSUInteger length = [headers length]; + // Header ID (1 byte) + [headers appendBytes:&headerID length:1]; + // Header value (4 bytes) + const uint32_t valueSwapped(NSSwapHostIntToBig(headerValue)); + [headers appendBytes:&valueSwapped length:sizeof valueSwapped]; + + return [headers length] - length == 5; +} + +bool append_unicode_header(ObjCStrongReference<NSMutableData> headers, uint8_t headerID, + const QString &string) +{ + // Two bytes of length followed by a null-terminated + // Unicode text string. Length is unsigned integer, + // it covers the header ID and the whole of the header + // value, including the length bytes and the two bytes + // of null terminator. + // All Obj-C objects are autoreleased. + + if (!headers) + return false; + + QT_BT_MAC_AUTORELEASEPOOL; + + const NSUInteger initialLength = [headers length]; + [headers appendBytes:&headerID length:1]; + + if (!string.length()) { + // Empty string. The length is 3 + // (header ID + length value itself). + return append_uint16(headers, 3); + } + + NSString *const nsString = string.toNSString(); + if (!nsString) + return false; + + // TODO: check if the encodings is right. It was NSUnicodeStringEncoding but + // byte order was wrong. Also, I do not need BOM check anymore? + NSData *const data = [nsString dataUsingEncoding:NSUTF16BigEndianStringEncoding]; + if (!data) + return false; + + // This data can include byte-order marker (BOM) and does not include + // a null terminator. Anyway, the length must be >= 2. + NSUInteger length = [data length]; + if (length < 2) + return false; + + const uint8_t *dataPtr = static_cast<const uint8_t *>([data bytes]); + if ((dataPtr[0] == 0xff && dataPtr[1] == 0xfe) + || (dataPtr[0] == 0xfe && dataPtr[1] == 0xff)) { + if (length == 2) //Something weird? + return false; + // Skip a BOM. + dataPtr += 2; + length -= 2; + } + + // headerID + length == 3, string's length + 2 + // bytes for a null terminator. + if (!append_uint16(headers, length + 3 + 2)) + return false; + + [headers appendBytes:dataPtr length:length]; + const uint8_t nullTerminator[2] = {}; + [headers appendBytes:nullTerminator length:2]; + + return [headers length] - initialLength == length + 3 + 2; +} + +ObjCStrongReference<NSMutableData> next_data_chunk(QIODevice &inputStream, IOBluetoothOBEXSession *session, + NSUInteger headersLength, bool &isLast) +{ + // Work only for OBEX put (we request a specific payload length). + Q_ASSERT_X(session, Q_FUNC_INFO, "invalid OBEX session (nil)"); + + const OBEXMaxPacketLength packetSize = [session getAvailableCommandPayloadLength:kOBEXOpCodePut]; + if (!packetSize || headersLength >= packetSize) + return ObjCStrongReference<NSMutableData>(); + + const OBEXMaxPacketLength maxBodySize = packetSize - headersLength; + + QVector<char> block(maxBodySize); + const int realSize = inputStream.read(block.data(), block.size()); + if (realSize <= 0) { + // Well, either the last or an error. + isLast = true; + return ObjCStrongReference<NSMutableData>(); + } + + ObjCStrongReference<NSMutableData> chunk([NSMutableData dataWithBytes:block.data() + length:realSize], true); + if (chunk && [chunk length]) { + // If it actually was the last chunk + // of a length == maxBodySize, we'll + // send one more packet (empty though)? + isLast = [chunk length] < maxBodySize; + } + + return chunk; +} + +bool check_connect_event(const OBEXSessionEvent *e, OBEXError &error, OBEXOpCode &response) +{ + Q_ASSERT_X(e, Q_FUNC_INFO, "invalid event (null)"); + + // This function tries to extract either an error code or a + // server response code. "Good" event has type connect command respond + // and reponse code 0XA0. Everything else is a "bad" event and + // means connect failed. + + // If it's an error event - return the error. + // If it's connect response - extract the response code. + // If it's something else (is it possible?) - set general error. + + if (e->type == kOBEXSessionEventTypeError) { + error = e->u.errorData.error; + return false; + } if (e->type == kOBEXSessionEventTypeConnectCommandResponseReceived) { + // We can read response code only for such an event. + response = e->u.connectCommandResponseData.serverResponseOpCode; + return response == kOBEXResponseCodeSuccessWithFinalBit; + } else { + qCWarning(QT_BT_DARWIN) << "unexpected event type"; + error = kOBEXGeneralError; + return false; + } +} + +bool check_put_event(const OBEXSessionEvent *e, OBEXError &error, OBEXOpCode &response) +{ + Q_ASSERT_X(e, Q_FUNC_INFO, "invalid event (null)"); + + // See the comments above. + + if (e->type == kOBEXSessionEventTypeError) { + error = e->u.errorData.error; + return false; + } else if (e->type == kOBEXSessionEventTypePutCommandResponseReceived) { + response = e->u.putCommandResponseData.serverResponseOpCode; + return response == kOBEXResponseCodeContinueWithFinalBit || + response == kOBEXResponseCodeSuccessWithFinalBit; + } else { + qCWarning(QT_BT_DARWIN) << "unexpected event type"; + error = kOBEXGeneralError; + return false; + } +} + +bool check_abort_event(const OBEXSessionEvent *e, OBEXError &error, OBEXOpCode &response) +{ + Q_ASSERT_X(e, Q_FUNC_INFO, "invalid event (null)"); + + if (e->type == kOBEXSessionEventTypeError) { + error = e->u.errorData.error; + return false; + } else if (e->type == kOBEXSessionEventTypeAbortCommandResponseReceived) { + response = e->u.abortCommandResponseData.serverResponseOpCode; + return response == kOBEXResponseCodeSuccessWithFinalBit; + } else { + qCWarning(QT_BT_DARWIN) << "unexpected event type"; + return false; + } +} + +} // Unnamed namespace. +} // namespace DarwinBluetooth. + +QT_END_NAMESPACE + +QT_USE_NAMESPACE + +@interface QT_MANGLE_NAMESPACE(DarwinBTOBEXSession) (PrivateAPI) + +// OBEXDisconnect returns void - it's considered to be always +// successful. These methods are "private API" - no need to expose them, +// for internal use only. +- (void)OBEXDisconnect; +- (void)OBEXDisconnectHandler:(const OBEXSessionEvent*)event; + +@end + +@implementation QT_MANGLE_NAMESPACE(DarwinBTOBEXSession) +{ + QT_PREPEND_NAMESPACE(DarwinBluetooth)::OBEXSessionDelegate *delegate; + IOBluetoothDevice *device; + quint16 channelID; + IOBluetoothOBEXSession *session; + + QT_PREPEND_NAMESPACE(DarwinBluetooth)::OBEXRequest currentRequest; + + bool connected; + bool connectionIDFound; + quint32 connectionID; + + QT_PREPEND_NAMESPACE(QIODevice) *inputStream; + + // TODO: switch to scoped pointers or strong reference objects instead. + NSMutableData *headersData; + NSMutableData *bodyData; + + quint32 bytesSent; + bool pendingAbort; +} + ++ (OBEXMaxPacketLength) maxPacketLength +{ + // Some arbitrary number, we'll adjust it as soon as + // we connected, asking a session about packet size for + // a particular command. + return 0x1000; +} + +- (id)initWithDelegate:(QT_PREPEND_NAMESPACE(DarwinBluetooth::OBEXSessionDelegate) *)aDelegate + remoteDevice:(const QBluetoothAddress &)deviceAddress channelID:(quint16)port +{ + Q_ASSERT_X(aDelegate, Q_FUNC_INFO, "invalid delegate (null)"); + Q_ASSERT_X(!deviceAddress.isNull(), Q_FUNC_INFO, "invalid remote device address"); + Q_ASSERT_X(port, Q_FUNC_INFO, "invalid port (0)"); + + if (self = [super init]) { + connected = false; + currentRequest = DarwinBluetooth::OBEXNoop; + connectionID = 0; + connectionIDFound = false; + + const BluetoothDeviceAddress addr(DarwinBluetooth::iobluetooth_address(deviceAddress)); + device = [[IOBluetoothDevice deviceWithAddress:&addr] retain]; + if (!device) { + qCWarning(QT_BT_DARWIN) << "failed to create an IOBluetoothDevice"; + return self; + } + + session = [[IOBluetoothOBEXSession alloc] initWithDevice:device channelID:port]; + if (!session) { + qCWarning(QT_BT_DARWIN) << "failed to create an OBEX session"; + return self; + } + + delegate = aDelegate; + channelID = port; + } + + return self; +} + +- (void)dealloc +{ + [device release]; + [session release]; + + [headersData release]; + [bodyData release]; + + [super dealloc]; +} + +- (OBEXError)OBEXConnect +{ + if (!session) { + qCWarning(QT_BT_DARWIN) << "invalid session (nil)"; + return kOBEXGeneralError; + } + + // That's a "single-shot" operation: + Q_ASSERT_X(currentRequest == DarwinBluetooth::OBEXNoop, Q_FUNC_INFO, + "can not connect in this state (another request is active)"); + + connected = false; + connectionIDFound = false; + connectionID = 0; + currentRequest = DarwinBluetooth::OBEXConnect; + + const OBEXError status = [session OBEXConnect:kOBEXConnectFlagNone + maxPacketLength:[QT_MANGLE_NAMESPACE(DarwinBTOBEXSession) maxPacketLength] + optionalHeaders:nullptr + optionalHeadersLength:0 + eventSelector:@selector(OBEXConnectHandler:) + selectorTarget:self + refCon:nullptr]; + + if (status != kOBEXSuccess) { + currentRequest = DarwinBluetooth::OBEXNoop; + // Already connected is still ok for us? + connected = status == kOBEXSessionAlreadyConnectedError; + } + + return status; +} + +- (void)OBEXConnectHandler:(const OBEXSessionEvent*)event +{ + using namespace DarwinBluetooth; + + Q_ASSERT_X(session, Q_FUNC_INFO, "invalid session (nil)"); + + if (pendingAbort) { + currentRequest = OBEXNoop; + [self OBEXAbort]; + return; + } + + if (currentRequest != OBEXConnect) { + qCWarning(QT_BT_DARWIN) << "called while there is no " + "active connect request"; + return; + } + + currentRequest = OBEXNoop; + + OBEXError errorCode = kOBEXSuccess; + OBEXOpCode responseCode = kOBEXResponseCodeSuccessWithFinalBit; + + if (!check_connect_event(event, errorCode, responseCode)) { + // OBEX connect failed. + if (delegate) + delegate->OBEXConnectError(errorCode, responseCode); + return; + } + + const OBEXConnectCommandResponseData *const response = &event->u.connectCommandResponseData; + if (response->headerDataPtr && response->headerDataLength >= 2) { + // 2 == 1 byte headerID + at least 1 byte headerValue ... + const QList<OBEXHeader> headers(qt_bluetooth_headers(static_cast<const uint8_t *>(response->headerDataPtr), + response->headerDataLength)); + // ConnectionID is used when multiplexing OBEX connections + // to identify which particular connection this object is + // being sent on. When used, this _must_ be the first + // header sent. + + for (const OBEXHeader &header : headers) { + if (header.headerID == kOBEXHeaderIDConnectionID) { + connectionID = header.value.value<quint32>(); + connectionIDFound = true; + break; + } + } + } + + connected = true; + + if (delegate) + delegate->OBEXConnectSuccess(); +} + +- (OBEXError)OBEXAbort +{ + using namespace DarwinBluetooth; + + Q_ASSERT_X(session, Q_FUNC_INFO, "invalid OBEX session (nil)"); + + if (currentRequest == OBEXNoop) { + pendingAbort = false; + + if (![self isConnected]) + return kOBEXSessionNotConnectedError; + + currentRequest = OBEXAbort; + const OBEXError status = [session OBEXAbort:nullptr + optionalHeadersLength:0 + eventSelector:@selector(OBEXAbortHandler:) + selectorTarget:self + refCon:nullptr]; + if (status != kOBEXSuccess) + currentRequest = OBEXNoop; + + return status; + } else { + // We're in the middle of some request, wait + // for any handler to be called first. + pendingAbort = true; + return kOBEXSuccess; + } +} + +- (void)OBEXAbortHandler:(const OBEXSessionEvent*)event +{ + using namespace DarwinBluetooth; + + Q_ASSERT_X(session, Q_FUNC_INFO, "invalid OBEX session (nil)"); + + if (currentRequest != OBEXAbort) { + qCWarning(QT_BT_DARWIN) << "called while there " + "is no ABORT request"; + return; + } + + pendingAbort = false; + currentRequest = OBEXNoop; + + if (delegate) { + OBEXError error = kOBEXSuccess; + OBEXOpCode response = kOBEXResponseCodeSuccessWithFinalBit; + if (check_abort_event(event, error, response)) + delegate->OBEXAbortSuccess(); + } +} + +- (OBEXError)OBEXPutFile:(QT_PREPEND_NAMESPACE(QIODevice) *)input withName:(const QString &)name +{ + using namespace DarwinBluetooth; + + if (!session || ![self isConnected]) + return kOBEXSessionNotConnectedError; + + Q_ASSERT_X(currentRequest == OBEXNoop, Q_FUNC_INFO, + "the current session has an active request already"); + Q_ASSERT_X(input, Q_FUNC_INFO, "invalid input stream (null)"); + Q_ASSERT_X(input->isReadable(), Q_FUNC_INFO, "invalid input stream (not readable)"); + + // We send a put request with a couple of headers (size/file name/may be connection ID) + + // a payload. + const qint64 fileSize = input->size(); + if (fileSize <= 0 || fileSize >= std::numeric_limits<uint32_t>::max()) { + qCWarning(QT_BT_DARWIN) << "invalid input file size"; + return kOBEXBadArgumentError; + } + + ObjCStrongReference<NSMutableData> headers([[NSMutableData alloc] init], false); + if (!headers) { + qCWarning(QT_BT_DARWIN) << "failed to allocate headers"; + return kOBEXNoResourcesError; + } + + // Now we append headers with: Connection ID (if any), + // file name, file size, the first (and probably the only) chunk of data + // from the input stream and send a put request. + + if (connectionIDFound) { + if (!append_four_byte_header(headers, kOBEXHeaderIDConnectionID, connectionID)) { + qCWarning(QT_BT_DARWIN) << "failed to append connection ID header"; + return kOBEXNoResourcesError; + } + } + + if (name.length()) { + if (!append_unicode_header(headers, kOBEXHeaderIDName, name)) { + qCWarning(QT_BT_DARWIN) << "failed to append a unicode string"; + return kOBEXNoResourcesError; + } + } + + if (fileSize && !input->isSequential()) + append_four_byte_header(headers, kOBEXHeaderIDLength, uint32_t(fileSize)); + + bool lastChunk = false; + ObjCStrongReference<NSMutableData> chunk(next_data_chunk(*input, session, [headers length], lastChunk)); + if (!chunk || ![chunk length]) { + // We do not support PUT-DELETE (?) + // At least the first chunk is expected to be non-empty. + qCWarning(QT_BT_DARWIN) << "invalid input stream"; + return kOBEXBadArgumentError; + } + + currentRequest = OBEXPut; + + const OBEXError status = [session OBEXPut:lastChunk + headersData:[headers mutableBytes] + headersDataLength:[headers length] + bodyData:[chunk mutableBytes] + bodyDataLength:[chunk length] + eventSelector:@selector(OBEXPutHandler:) + selectorTarget:self + refCon:nullptr]; + + if (status == kOBEXSuccess) { + if (delegate && fileSize && !input->isSequential()) + delegate->OBEXPutDataSent([chunk length], fileSize); + + bytesSent = [chunk length]; + headersData = headers.take(); + bodyData = chunk.take(); + inputStream = input; + } else { + // PUT request failed and we now + // want to close a connection/session. + currentRequest = OBEXNoop; + // Try to cleanup (disconnect). + [self OBEXDisconnect]; + } + + return status; +} + +- (void)OBEXPutHandler:(const OBEXSessionEvent*)event +{ + using namespace DarwinBluetooth; + + Q_ASSERT_X(session, Q_FUNC_INFO, "invalid OBEX session (nil)"); + + if (pendingAbort) { + currentRequest = OBEXNoop; + [self OBEXAbort]; + return; + } + + if (currentRequest != OBEXPut) { + qCWarning(QT_BT_DARWIN) << "called while the current " + "request is not a put request"; + return; + } + + OBEXError error = kOBEXSuccess; + OBEXOpCode responseCode = kOBEXResponseCodeSuccessWithFinalBit; + if (!check_put_event(event, error, responseCode)) { + currentRequest = OBEXNoop; + if (delegate) + delegate->OBEXPutError(error, responseCode); + [self OBEXDisconnect]; + return; + } + + // Now try to send more data if we have any. + if (responseCode == kOBEXResponseCodeContinueWithFinalBit) { + // Send more data. + bool lastChunk = false; + // 0 for the headers length, no more headers. + ObjCStrongReference<NSMutableData> chunk(next_data_chunk(*inputStream, session, 0, lastChunk)); + if (!chunk && !lastChunk) { + qCWarning(QT_BT_DARWIN) << "failed to allocate the next memory chunk"; + return; + } + + void *dataPtr = chunk ? [chunk mutableBytes] : nullptr; + const NSUInteger dataSize = chunk ? [chunk length] : 0; + const OBEXError status = [session OBEXPut:lastChunk + headersData:nullptr + headersDataLength:0 + bodyData:dataPtr + bodyDataLength:dataSize + eventSelector:@selector(OBEXPutHandler:) + selectorTarget:self + refCon:nullptr]; + + if (status != kOBEXSuccess) { + qCWarning(QT_BT_DARWIN) << "failed to send the next memory chunk"; + currentRequest = OBEXNoop; + if (delegate) // Response code is not important here. + delegate->OBEXPutError(kOBEXNoResourcesError, 0); + + [self OBEXDisconnect]; + } else { + [bodyData release]; + bytesSent += [chunk length]; + bodyData = chunk.take();//retained already. + + if (delegate && !inputStream->isSequential()) + delegate->OBEXPutDataSent(bytesSent, inputStream->size()); + } + } else if (responseCode == kOBEXResponseCodeSuccessWithFinalBit) { + currentRequest = OBEXNoop; + if (delegate) + delegate->OBEXPutSuccess(); + + [self OBEXDisconnect]; + } +} + +- (void)OBEXDisconnect +{ + Q_ASSERT_X(session, Q_FUNC_INFO, "invalid session (nil)"); + + currentRequest = DarwinBluetooth::OBEXDisconnect; + + [session OBEXDisconnect:nullptr + optionalHeadersLength:0 + eventSelector:@selector(OBEXDisconnectHandler:) + selectorTarget:self + refCon:nullptr]; +} + +- (void)OBEXDisconnectHandler:(const OBEXSessionEvent*)event +{ + Q_UNUSED(event) + + Q_ASSERT_X(session, Q_FUNC_INFO, "invalid session (nil)"); + + // Event can have an error type, but there's nothing + // we can do - even "cleanup" failed. + connected = false; +} + +- (bool)isConnected +{ + return device && session && connected; +} + +- (void)closeSession +{ + // Clear the delegate and reset the request, + // do not try any of OBEX commands - the session will be deleted + // immediately. + delegate = nullptr; + // This will stop any handler (callback) preventing + // any read/write to potentially deleted objects. + currentRequest = DarwinBluetooth::OBEXNoop; +} + +- (bool)hasActiveRequest +{ + return currentRequest != DarwinBluetooth::OBEXNoop && !pendingAbort; +} + +@end diff --git a/src/bluetooth/darwin/btobexsession_p.h b/src/bluetooth/darwin/btobexsession_p.h new file mode 100644 index 00000000..11c6d226 --- /dev/null +++ b/src/bluetooth/darwin/btobexsession_p.h @@ -0,0 +1,138 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#ifndef BTOBEXSESSION_P_H +#define BTOBEXSESSION_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/qvariant.h> +#include <QtCore/qglobal.h> + +#include <Foundation/Foundation.h> + +#include <IOBluetooth/IOBluetooth.h> + +// TODO: all this code must be removed in Qt 6? + +@class QT_MANGLE_NAMESPACE(DarwinBTOBEXSession); + +QT_BEGIN_NAMESPACE + +class QBluetoothAddress; +class QIODevice; +class QString; + +namespace DarwinBluetooth +{ + +class OBEXSessionDelegate +{ +public: + typedef QT_MANGLE_NAMESPACE(DarwinBTOBEXSession) ObjCOBEXSession; + + virtual ~OBEXSessionDelegate(); + + virtual void OBEXConnectError(OBEXError error, OBEXOpCode responseCode) = 0; + virtual void OBEXConnectSuccess() = 0; + + virtual void OBEXAbortSuccess() = 0; + + virtual void OBEXPutDataSent(quint32 current, quint32 total) = 0; + virtual void OBEXPutSuccess() = 0; + virtual void OBEXPutError(OBEXError error, OBEXOpCode responseCode) = 0; +}; + +enum OBEXRequest { + OBEXNoop, + OBEXConnect, + OBEXDisconnect, + OBEXPut, + OBEXGet, + OBEXSetPath, + OBEXAbort +}; + +} // namespace DarwinBluetooth + +QT_END_NAMESPACE + +// OBEX Session, it's a "single-shot" operation as our QBluetoothTransferReply is +// (it does not have an interface to re-send data or re-use the same transfer reply). +// It either succeeds or fails and tries to cleanup in any case. +@interface QT_MANGLE_NAMESPACE(DarwinBTOBEXSession) : NSObject + +- (id)initWithDelegate:(QT_PREPEND_NAMESPACE(DarwinBluetooth::OBEXSessionDelegate) *)aDelegate + remoteDevice:(const QBluetoothAddress &)deviceAddress channelID:(quint16)port; + +- (void)dealloc; + +// Below I have pairs: OBEX operation and its callback method. +- (OBEXError)OBEXConnect; +- (void)OBEXConnectHandler:(const OBEXSessionEvent*)event; + +- (OBEXError)OBEXAbort; +- (void)OBEXAbortHandler:(const OBEXSessionEvent*)event; + +- (OBEXError)OBEXPutFile:(QT_PREPEND_NAMESPACE(QIODevice) *)inputStream withName:(const QString &)name; +- (void)OBEXPutHandler:(const OBEXSessionEvent*)event; + +// Aux. methods. +- (bool)isConnected; + +// To be called from C++ destructors. OBEXSession is not +// valid anymore after this call (no more OBEX operations +// can be executed). It's an ABORT/DISCONNECT sequence. +// It also resets a delegate to null. +- (void)closeSession; +// +- (bool)hasActiveRequest; + +@end + +#endif diff --git a/src/bluetooth/darwin/btperipheralmanager.mm b/src/bluetooth/darwin/btperipheralmanager.mm new file mode 100644 index 00000000..d4713ace --- /dev/null +++ b/src/bluetooth/darwin/btperipheralmanager.mm @@ -0,0 +1,897 @@ +/**************************************************************************** +** +** 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 "qlowenergycharacteristicdata.h" +#include "qlowenergydescriptordata.h" +#include "btperipheralmanager_p.h" +#include "qlowenergyservicedata.h" +#include "btnotifier_p.h" +#include "qbluetooth.h" + +#include <QtCore/qdebug.h> +#include <QtCore/qlist.h> + +#include <algorithm> +#include <limits> + +namespace +{ + +CBCharacteristicProperties cb_properties(const QLowEnergyCharacteristicData &data) +{ + // Direct 'mapping' is ok. + return CBCharacteristicProperties(int(data.properties())); +} + +CBAttributePermissions cb_permissions(const QLowEnergyCharacteristicData &data) +{ + using QLEC = QLowEnergyCharacteristic; + + const auto props = data.properties(); + CBAttributePermissions cbFlags = {}; + + if ((props & QLEC::Write) || (props & QLEC::WriteNoResponse) + || (props & QLEC::WriteSigned)) { + cbFlags = CBAttributePermissions(cbFlags | CBAttributePermissionsWriteable); + } + + if (props & QLEC::Read) + cbFlags = CBAttributePermissions(cbFlags | CBAttributePermissionsReadable); + + if (data.writeConstraints() & QBluetooth::AttEncryptionRequired) + cbFlags = CBAttributePermissions(cbFlags | CBAttributePermissionsWriteEncryptionRequired); + + if (data.readConstraints() & QBluetooth::AttEncryptionRequired) + cbFlags = CBAttributePermissions(cbFlags | CBAttributePermissionsReadEncryptionRequired); + + return cbFlags; +} + +ObjCStrongReference<CBMutableCharacteristic> create_characteristic(const QLowEnergyCharacteristicData &data) +{ + const ObjCStrongReference<CBMutableCharacteristic> ch([[CBMutableCharacteristic alloc] initWithType:cb_uuid(data.uuid()) + properties:cb_properties(data) + value:nil + permissions:cb_permissions(data)], + false /*do not retain*/); + return ch; +} + +ObjCStrongReference<CBMutableDescriptor> create_descriptor(const QLowEnergyDescriptorData &data) +{ + // CoreBluetooth supports only: + /* + "That said, only two of these are currently supported when creating local, + mutable descriptors: the characteristic user description descriptor and + the characteristic format descriptor, represented by the CBUUID constants + CBUUIDCharacteristicUserDescriptionString and CBUUIDCharacteristicFormatString" + */ + + if (data.uuid() != QBluetoothUuid::CharacteristicUserDescription && + data.uuid() != QBluetoothUuid::CharacteristicPresentationFormat) { + qCWarning(QT_BT_DARWIN) << "unsupported descriptor" << data.uuid(); + return {}; + } + + QT_BT_MAC_AUTORELEASEPOOL + + // Descriptors are immutable with CoreBluetooth, that's why we + // have to provide a value here and not able to change it later. + ObjCStrongReference<NSObject> value; + if (data.uuid() == QBluetoothUuid::CharacteristicUserDescription) { + const QString asQString(QString::fromUtf8(data.value())); + value.reset(asQString.toNSString()); + } else { + const auto nsData = data_from_bytearray(data.value()); + value.reset(nsData.data()); + } + + const ObjCStrongReference<CBMutableDescriptor> d([[CBMutableDescriptor alloc] + initWithType:cb_uuid(data.uuid()) + value:value], false /*do not retain*/); + return d; +} + +quint32 qt_countGATTEntries(const QLowEnergyServiceData &data) +{ + const auto maxu32 = std::numeric_limits<quint32>::max(); + // + 1 for a service itself. + quint32 nEntries = 1 + quint32(data.includedServices().count()); + for (const auto &ch : data.characteristics()) { + if (maxu32 - 2 < nEntries) + return {}; + nEntries += 2; + if (maxu32 - ch.descriptors().count() < nEntries) + return {}; + nEntries += ch.descriptors().count(); + } + + return nEntries; +} + +bool qt_validate_value_range(const QLowEnergyCharacteristicData &data) +{ + if (data.minimumValueLength() > data.maximumValueLength() + || data.minimumValueLength() < 0) { + return false; + } + + return data.value().size() <= data.maximumValueLength(); +} + +} + +@interface QT_MANGLE_NAMESPACE(DarwinBTPeripheralManager) (PrivateAPI) + +- (void)addConnectedCentral:(CBCentral *)central; +- (void)removeConnectedCentral:(CBCentral *)central; +- (CBService *)findIncludedService:(const QBluetoothUuid &)qtUUID; + +- (void)addIncludedServices:(const QLowEnergyServiceData &)data + to:(CBMutableService *)cbService + qtService:(QLowEnergyServicePrivate *)qtService; + +- (void)addCharacteristicsAndDescriptors:(const QLowEnergyServiceData &)data + to:(CBMutableService *)cbService + qtService:(QLowEnergyServicePrivate *)qtService; + +- (CBATTError)validateWriteRequest:(CBATTRequest *)request; + +@end + +@implementation QT_MANGLE_NAMESPACE(DarwinBTPeripheralManager) +{ + ObjCScopedPointer<CBPeripheralManager> manager; + LECBManagerNotifier *notifier; + + QLowEnergyHandle lastHandle; + // Services in this vector are placed in such order: + // the one that has included services, must + // follow its included services to avoid exceptions from CBPeripheralManager. + std::vector<ObjCStrongReference<CBMutableService>> services; + decltype(services.size()) nextServiceToAdd; + + // Lookup map for included services: + std::map<QBluetoothUuid, CBService *> serviceIndex; + ObjCScopedPointer<NSMutableDictionary> advertisementData; + + GenericLEMap<CBCharacteristic *> charMap; + GenericLEMap<ObjCStrongReference<NSMutableData>> charValues; + + QMap<QLowEnergyHandle, ValueRange> valueRanges; + + std::deque<UpdateRequest> updateQueue; + + ObjCScopedPointer<NSMutableSet> connectedCentrals; + + PeripheralState state; + NSUInteger maxNotificationValueLength; +} + +- (id)initWith:(LECBManagerNotifier *)aNotifier +{ + if (self = [super init]) { + Q_ASSERT(aNotifier); + notifier = aNotifier; + state = PeripheralState::idle; + nextServiceToAdd = {}; + connectedCentrals.reset([[NSMutableSet alloc] init]); + maxNotificationValueLength = std::numeric_limits<NSUInteger>::max(); + } + + return self; +} + +- (void)dealloc +{ + [self detach]; + [super dealloc]; +} + +- (QSharedPointer<QLowEnergyServicePrivate>)addService:(const QLowEnergyServiceData &)data +{ + using QLES = QLowEnergyService; + + const auto nEntries = qt_countGATTEntries(data); + if (!nEntries || nEntries > std::numeric_limits<QLowEnergyHandle>::max() - lastHandle) { + qCCritical(QT_BT_DARWIN) << "addService: not enough handles"; + return {}; + } + + QT_BT_MAC_AUTORELEASEPOOL + + const BOOL primary = data.type() == QLowEnergyServiceData::ServiceTypePrimary; + const auto cbUUID = cb_uuid(data.uuid()); + + const ObjCStrongReference<CBMutableService> + newCBService([[CBMutableService alloc] initWithType:cbUUID primary:primary], + false /*do not retain*/); + + if (!newCBService) { + qCCritical(QT_BT_DARWIN) << "addService: failed to create CBMutableService"; + return {}; + } + + auto newQtService = QSharedPointer<QLowEnergyServicePrivate>::create(); + newQtService->state = QLowEnergyService::LocalService; + newQtService->uuid = data.uuid(); + newQtService->type = primary ? QLES::PrimaryService : QLES::IncludedService; + newQtService->startHandle = ++lastHandle; + // Controller will be set by ... controller :) + + [self addIncludedServices:data to:newCBService qtService:newQtService.data()]; + [self addCharacteristicsAndDescriptors:data to:newCBService qtService:newQtService.data()]; + + services.push_back(newCBService); + serviceIndex[data.uuid()] = newCBService; + + newQtService->endHandle = lastHandle; + + return newQtService; +} + +- (void) setParameters:(const QLowEnergyAdvertisingParameters &)parameters + data:(const QLowEnergyAdvertisingData &)data + scanResponse:(const QLowEnergyAdvertisingData &)scanResponse +{ + Q_UNUSED(parameters) + + // This is the last method we call on the controller's thread + // before starting advertising on the Qt's LE queue. + // From Apple's docs: + /* + - (void)startAdvertising:(NSDictionary *)advertisementData + + Advertises peripheral manager data. + + * advertisementData + + - An optional dictionary containing the data you want to advertise. + The possible keys of an advertisementData dictionary are detailed in CBCentralManagerDelegate + Protocol Reference. That said, only two of the keys are supported for peripheral manager objects: + CBAdvertisementDataLocalNameKey and CBAdvertisementDataServiceUUIDsKey. + */ + + QT_BT_MAC_AUTORELEASEPOOL + + advertisementData.reset([[NSMutableDictionary alloc] init]); + if (!advertisementData) { + qCWarning(QT_BT_DARWIN) << "setParameters: failed to allocate " + "NSMutableDictonary (advertisementData)"; + return; + } + + auto localName = scanResponse.localName(); + if (!localName.size()) + localName = data.localName(); + + if (localName.size()) { + [advertisementData setObject:localName.toNSString() + forKey:CBAdvertisementDataLocalNameKey]; + } + + if (!data.services().count() && !scanResponse.services().count()) + return; + + const ObjCScopedPointer<NSMutableArray> uuids([[NSMutableArray alloc] init]); + if (!uuids) { + qCWarning(QT_BT_DARWIN) << "setParameters: failed to allocate " + "NSMutableArray (services uuids)"; + return; + } + + + for (const auto &qtUUID : data.services()) { + const auto cbUUID = cb_uuid(qtUUID); + if (cbUUID) + [uuids addObject:cbUUID]; + } + + for (const auto &qtUUID : scanResponse.services()) { + const auto cbUUID = cb_uuid(qtUUID); + if (cbUUID) + [uuids addObject:cbUUID]; + } + + if ([uuids count]) { + [advertisementData setObject:uuids + forKey:CBAdvertisementDataServiceUUIDsKey]; + } +} + +- (void)startAdvertising +{ + state = PeripheralState::waitingForPowerOn; + if (manager.data()) + [manager setDelegate:nil]; + manager.reset([[CBPeripheralManager alloc] initWithDelegate:self + queue:DarwinBluetooth::qt_LE_queue()]); +} + +- (void)stopAdvertising +{ + [manager stopAdvertising]; + state = PeripheralState::idle; +} + +- (void)detach +{ + if (notifier) { + notifier->disconnect(); + notifier->deleteLater(); + notifier = nullptr; + } + + if (state == PeripheralState::advertising) { + [manager stopAdvertising]; + [manager setDelegate:nil]; + state = PeripheralState::idle; + } +} + +- (void)write:(const QByteArray &)value + charHandle:(QLowEnergyHandle)charHandle +{ + if (!notifier) + return; + + QT_BT_MAC_AUTORELEASEPOOL + + if (!charMap.contains(charHandle) || !valueRanges.contains(charHandle)) { + emit notifier->CBManagerError(QLowEnergyController::UnknownError); + return; + } + + const auto & range = valueRanges[charHandle]; + if (value.size() < int(range.first) || value.size() > int(range.second) +#ifdef Q_OS_IOS + || value.size() > DarwinBluetooth::maxValueLength) { +#else + ) { +#endif + qCWarning(QT_BT_DARWIN) << "ignoring value of invalid length" << value.size(); + return; + } + + emit notifier->characteristicWritten(charHandle, value); + + const auto nsData = mutable_data_from_bytearray(value); + charValues[charHandle] = nsData; + // We copy data here: sending update requests is async (see sendUpdateRequests), + // by the time we're allowed to actually send them, the data can change again + // and we'll send an 'out of order' value. + const ObjCStrongReference<NSData> copy([NSData dataWithData:nsData], true); + updateQueue.push_back(UpdateRequest{charHandle, copy}); + [self sendUpdateRequests]; +} + +- (void) addServicesToPeripheral +{ + Q_ASSERT(manager.data()); + + if (nextServiceToAdd < services.size()) + [manager addService:services[nextServiceToAdd++]]; +} + +// CBPeripheralManagerDelegate: + +- (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral +{ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunguarded-availability-new" + + if (peripheral != manager || !notifier) + return; + +#if QT_IOS_PLATFORM_SDK_EQUAL_OR_ABOVE(__IPHONE_10_0) || QT_OSX_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_13) + if (peripheral.state == CBManagerStatePoweredOn) { +#else + if (peripheral.state == CBPeripheralManagerStatePoweredOn) { +#endif + // "Bluetooth is currently powered on and is available to use." + if (state == PeripheralState::waitingForPowerOn) { + [manager removeAllServices]; + nextServiceToAdd = {}; + state = PeripheralState::advertising; + [self addServicesToPeripheral]; + } + return; + } + + /* + "A state with a value lower than CBPeripheralManagerStatePoweredOn implies that + advertising has stopped and that any connected centrals have been disconnected." + */ + + [connectedCentrals removeAllObjects]; + + if (state == PeripheralState::advertising) { + state = PeripheralState::waitingForPowerOn; + } else if (state == PeripheralState::connected) { + state = PeripheralState::idle; + emit notifier->disconnected(); + } + + // The next four states are _below_ "powered off"; according to the docs: + /* + "In addition, the local database is cleared and all services must be + explicitly added again." + */ + +#if QT_IOS_PLATFORM_SDK_EQUAL_OR_ABOVE(__IPHONE_10_0) || QT_OSX_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_13) + if (peripheral.state == CBManagerStateUnauthorized || + peripheral.state == CBManagerStateUnsupported) { +#else + if (peripheral.state == CBPeripheralManagerStateUnauthorized || + peripheral.state == CBPeripheralManagerStateUnsupported) { +#endif + emit notifier->LEnotSupported(); + state = PeripheralState::idle; + } + +#pragma clang diagnostic pop +} + +- (void)peripheralManager:(CBPeripheralManager *)peripheral + willRestoreState:(NSDictionary *)dict +{ + Q_UNUSED(peripheral) + Q_UNUSED(dict) + // NOOP atm. +} + +- (void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheral + error:(NSError *)error +{ + if (peripheral != manager || !notifier) + return; + + if (error) { + NSLog(@"failed to start advertising, error: %@", error); + state = PeripheralState::idle; + emit notifier->CBManagerError(QLowEnergyController::AdvertisingError); + } +} + +- (void)peripheralManager:(CBPeripheralManager *)peripheral + didAddService:(CBService *)service error:(NSError *)error +{ + Q_UNUSED(service) + + if (peripheral != manager || !notifier) + return; + + if (error) { + NSLog(@"failed to add a service, error: %@", error); + emit notifier->CBManagerError(QLowEnergyController::AdvertisingError); + state = PeripheralState::idle; + return; + } + + if (nextServiceToAdd == services.size()) + [manager startAdvertising:[advertisementData count] ? advertisementData.data() : nil]; + else + [self addServicesToPeripheral]; +} + +- (void)peripheralManager:(CBPeripheralManager *)peripheral central:(CBCentral *)central + didSubscribeToCharacteristic:(CBCharacteristic *)characteristic +{ + Q_UNUSED(characteristic) + + if (peripheral != manager || !notifier) + return; + + [self addConnectedCentral:central]; + + if (const auto handle = charMap.key(characteristic)) + emit notifier->notificationEnabled(handle, true); +} + +- (void)peripheralManager:(CBPeripheralManager *)peripheral central:(CBCentral *)central + didUnsubscribeFromCharacteristic:(CBCharacteristic *)characteristic +{ + Q_UNUSED(characteristic) + + if (peripheral != manager || !notifier) + return; + + [self removeConnectedCentral:central]; + + if (![connectedCentrals count]) { + if (const auto handle = charMap.key(characteristic)) + emit notifier->notificationEnabled(handle, false); + } +} + +- (void)peripheralManager:(CBPeripheralManager *)peripheral + didReceiveReadRequest:(CBATTRequest *)request +{ + if (peripheral != manager || !notifier) + return; + + QT_BT_MAC_AUTORELEASEPOOL + + const auto handle = charMap.key(request.characteristic); + if (!handle || !charValues.contains(handle)) { + qCWarning(QT_BT_DARWIN) << "invalid read request, unknown characteristic"; + [manager respondToRequest:request withResult:CBATTErrorInvalidHandle]; + return; + } + + const auto &value = charValues[handle]; + if (request.offset > [value length]) { + qCWarning(QT_BT_DARWIN) << "invalid offset in a read request"; + [manager respondToRequest:request withResult:CBATTErrorInvalidOffset]; + return; + } + + [self addConnectedCentral:request.central]; + + NSData *dataToSend = nil; + if (!request.offset) { + dataToSend = value; + } else { + dataToSend = [value subdataWithRange: + NSMakeRange(request.offset, [value length] - request.offset)]; + } + + request.value = dataToSend; + [manager respondToRequest:request withResult:CBATTErrorSuccess]; +} + + +- (void)writeValueForCharacteristic:(QLowEnergyHandle) charHandle + withWriteRequest:(CBATTRequest *)request +{ + Q_ASSERT(charHandle); + Q_ASSERT(request); + + Q_ASSERT(valueRanges.contains(charHandle)); + const auto &range = valueRanges[charHandle]; + Q_ASSERT(request.offset <= range.second + && request.value.length <= range.second - request.offset); + + Q_ASSERT(charValues.contains(charHandle)); + NSMutableData *const value = charValues[charHandle]; + if (request.offset + request.value.length > value.length) + [value increaseLengthBy:request.offset + request.value.length - value.length]; + + [value replaceBytesInRange:NSMakeRange(request.offset, request.value.length) + withBytes:request.value.bytes]; +} + +- (void)peripheralManager:(CBPeripheralManager *)peripheral + didReceiveWriteRequests:(NSArray *)requests +{ + QT_BT_MAC_AUTORELEASEPOOL + + if (peripheral != manager || !notifier) { + // Detached already. + return; + } + + // We first test if all requests are valid + // since CoreBluetooth requires "all or none" + // and respond only _once_ to the first one. + for (CBATTRequest *request in requests) { + const auto status = [self validateWriteRequest:request]; + if (status != CBATTErrorSuccess) { + [manager respondToRequest:[requests objectAtIndex:0] + withResult:status]; + return; + } + } + + std::map<QLowEnergyHandle, NSUInteger> updated; + + for (CBATTRequest *request in requests) { + // Transition to 'connected' if needed. + [self addConnectedCentral:request.central]; + const auto charHandle = charMap.key(request.characteristic); + const auto prevLen = updated[charHandle]; + updated[charHandle] = std::max(request.offset + request.value.length, + prevLen); + [self writeValueForCharacteristic:charHandle withWriteRequest:request]; + } + + for (const auto pair : updated) { + const auto handle = pair.first; + NSMutableData *value = charValues[handle]; + value.length = pair.second; + emit notifier->characteristicUpdated(handle, qt_bytearray(value)); + const ObjCStrongReference<NSData> copy([NSData dataWithData:value], + true); + updateQueue.push_back(UpdateRequest{handle, copy}); + } + + if (requests.count) { + [manager respondToRequest:[requests objectAtIndex:0] + withResult:CBATTErrorSuccess]; + } + + [self sendUpdateRequests]; +} + +- (void)peripheralManagerIsReadyToUpdateSubscribers:(CBPeripheralManager *)peripheral +{ + if (peripheral != manager || !notifier) { + // Detached. + return; + } + + [self sendUpdateRequests]; +} + +- (void)sendUpdateRequests +{ + QT_BT_MAC_AUTORELEASEPOOL + + while (updateQueue.size()) { + const auto &request = updateQueue.front(); + if (charMap.contains(request.charHandle)) { + if ([connectedCentrals count] + && maxNotificationValueLength < [request.value length]) { + qCWarning(QT_BT_DARWIN) << "value of length" << [request.value length] + << "will possibly be truncated to" + << maxNotificationValueLength; + } + const BOOL res = [manager updateValue:request.value + forCharacteristic:static_cast<CBMutableCharacteristic *>(charMap[request.charHandle]) + onSubscribedCentrals:nil]; + if (!res) { + // Have to wait for the 'ManagerIsReadyToUpdate'. + break; + } + } + + updateQueue.pop_front(); + } +} + +// Private API: + +- (void)addConnectedCentral:(CBCentral *)central +{ + if (!central) + return; + + if (!notifier) { + // We were detached. + return; + } + + maxNotificationValueLength = std::min(maxNotificationValueLength, + central.maximumUpdateValueLength); + + QT_BT_MAC_AUTORELEASEPOOL + + if (state == PeripheralState::advertising) { + state = PeripheralState::connected; + [manager stopAdvertising]; + emit notifier->connected(); + } + + if (![connectedCentrals containsObject:central.identifier]) + [connectedCentrals addObject:central.identifier]; +} + +- (void)removeConnectedCentral:(CBCentral *)central +{ + if (!notifier) { + // Detached. + return; + } + + QT_BT_MAC_AUTORELEASEPOOL + + if ([connectedCentrals containsObject:central.identifier]) + [connectedCentrals removeObject:central.identifier]; + + if (state == PeripheralState::connected && ![connectedCentrals count]) { + state = PeripheralState::idle; + emit notifier->disconnected(); + } + + if (![connectedCentrals count]) + maxNotificationValueLength = std::numeric_limits<NSUInteger>::max(); +} + +- (CBService *)findIncludedService:(const QBluetoothUuid &)qtUUID +{ + const auto it = serviceIndex.find(qtUUID); + if (it == serviceIndex.end()) + return nil; + + return it->second; +} + +- (void)addIncludedServices:(const QLowEnergyServiceData &)data + to:(CBMutableService *)cbService + qtService:(QLowEnergyServicePrivate *)qtService +{ + Q_ASSERT(cbService); + Q_ASSERT(qtService); + + QT_BT_MAC_AUTORELEASEPOOL + + ObjCScopedPointer<NSMutableArray> included([[NSMutableArray alloc] init]); + if (!included) { + qCWarning(QT_BT_DARWIN) << "addIncludedSerivces: failed " + "to allocate NSMutableArray"; + return; + } + + for (auto includedService : data.includedServices()) { + if (CBService *cbs = [self findIncludedService:includedService->serviceUuid()]) { + [included addObject:cbs]; + qtService->includedServices << includedService->serviceUuid(); + ++lastHandle; + } else { + qCWarning(QT_BT_DARWIN) << "can not use" << includedService->serviceUuid() + << "as included, it has to be added first"; + } + } + + if ([included count]) + cbService.includedServices = included; +} + +- (void)addCharacteristicsAndDescriptors:(const QLowEnergyServiceData &)data + to:(CBMutableService *)cbService + qtService:(QLowEnergyServicePrivate *)qtService +{ + Q_ASSERT(cbService); + Q_ASSERT(qtService); + + QT_BT_MAC_AUTORELEASEPOOL + + ObjCScopedPointer<NSMutableArray> newCBChars([[NSMutableArray alloc] init]); + if (!newCBChars) { + qCWarning(QT_BT_DARWIN) << "addCharacteristicsAndDescritptors: " + "failed to allocate NSMutableArray " + "(characteristics)"; + return; + } + + for (const auto &ch : data.characteristics()) { + if (!qt_validate_value_range(ch)) { + qCWarning(QT_BT_DARWIN) << "addCharacteristicsAndDescritptors: " + "invalid value size/min-max length"; + continue; + } + +#ifdef Q_OS_IOS + if (ch.value().length() > DarwinBluetooth::maxValueLength) { + qCWarning(QT_BT_DARWIN) << "addCharacteristicsAndDescritptors: " + "value exceeds the maximal permitted " + "value length (" + << DarwinBluetooth::maxValueLength + << "octets) on the platform"; + continue; + } +#endif + + const auto cbChar(create_characteristic(ch)); + if (!cbChar) { + qCWarning(QT_BT_DARWIN) << "addCharacteristicsAndDescritptors: " + "failed to allocate a characteristic"; + continue; + } + + const auto nsData(mutable_data_from_bytearray(ch.value())); + if (!nsData) { + qCWarning(QT_BT_DARWIN) << "addCharacteristicsAndDescritptors: " + "addService: failed to allocate NSData (char value)"; + continue; + } + + [newCBChars addObject:cbChar]; + + const auto declHandle = ++lastHandle; + // CB part: + charMap[declHandle] = cbChar; + charValues[declHandle] = nsData; + valueRanges[declHandle] = ValueRange(ch.minimumValueLength(), ch.maximumValueLength()); + // QT part: + QLowEnergyServicePrivate::CharData charData; + charData.valueHandle = declHandle; + charData.uuid = ch.uuid(); + charData.properties = ch.properties(); + charData.value = ch.value(); + + const ObjCScopedPointer<NSMutableArray> newCBDescs([[NSMutableArray alloc] init]); + if (!newCBDescs) { + qCWarning(QT_BT_DARWIN) << "addCharacteristicsAndDescritptors: " + "failed to allocate NSMutableArray " + "(descriptors)"; + continue; + } + + for (const auto &desc : ch.descriptors()) { + // CB part: + const auto cbDesc(create_descriptor(desc)); + const auto descHandle = ++lastHandle; + if (cbDesc) { + // See comments in create_descriptor on + // why cbDesc can be nil. + [newCBDescs addObject:cbDesc]; + } + // QT part: + QLowEnergyServicePrivate::DescData descData; + descData.uuid = desc.uuid(); + descData.value = desc.value(); + charData.descriptorList.insert(descHandle, descData); + } + + if ([newCBDescs count]) + cbChar.data().descriptors = newCBDescs.data(); // retains + + qtService->characteristicList.insert(declHandle, charData); + } + + if ([newCBChars count]) + cbService.characteristics = newCBChars.data(); +} + +- (CBATTError)validateWriteRequest:(CBATTRequest *)request +{ + Q_ASSERT(request); + + QT_BT_MAC_AUTORELEASEPOOL + + const auto handle = charMap.key(request.characteristic); + if (!handle || !charValues.contains(handle)) + return CBATTErrorInvalidHandle; + + Q_ASSERT(valueRanges.contains(handle)); + + const auto &range = valueRanges[handle]; + if (request.offset > range.second) + return CBATTErrorInvalidOffset; + + if (request.value.length > range.second - request.offset) + return CBATTErrorInvalidAttributeValueLength; + + return CBATTErrorSuccess; +} + +@end diff --git a/src/bluetooth/darwin/btperipheralmanager_p.h b/src/bluetooth/darwin/btperipheralmanager_p.h new file mode 100644 index 00000000..e09165ac --- /dev/null +++ b/src/bluetooth/darwin/btperipheralmanager_p.h @@ -0,0 +1,163 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#ifndef BTPERIPHERALMANAGER_P_H +#define BTPERIPHERALMANAGER_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of internal files. This header file may change from version to version +// without notice, or even be removed. +// +// We mean it. +// + +#include "btutility_p.h" + +#include "qlowenergyadvertisingparameters.h" +#include "qlowenergyserviceprivate_p.h" +#include "qbluetoothuuid.h" +#include "qbluetooth.h" + +#include <QtCore/qsharedpointer.h> +#include <QtCore/qbytearray.h> +#include <QtCore/qsysinfo.h> +#include <QtCore/qglobal.h> +#include <QtCore/qpair.h> +#include <QtCore/qmap.h> + +#include <vector> +#include <deque> +#include <map> + +#include <Foundation/Foundation.h> + +#include <CoreBluetooth/CoreBluetooth.h> + +QT_BEGIN_NAMESPACE + +class QLowEnergyServiceData; + +namespace DarwinBluetooth +{ + +class LECBManagerNotifier; + +} // namespace DarwinBluetooth + +QT_END_NAMESPACE + + +// Exposing names in a header is ugly, but constant QT_PREPEND_NAMESPACE is even worse ... +// After all, this header is to be included only in its own and controller's *.mm files. + +QT_USE_NAMESPACE + +using namespace DarwinBluetooth; + + +template<class Type> +using GenericLEMap = QMap<QLowEnergyHandle, Type>; + +enum class PeripheralState +{ + idle, + waitingForPowerOn, + advertising, + connected +}; + +struct UpdateRequest +{ + UpdateRequest() = default; + UpdateRequest(QLowEnergyHandle handle, const ObjCStrongReference<NSData> &val) + : charHandle(handle), + value(val) + { + } + + QLowEnergyHandle charHandle = {}; + ObjCStrongReference<NSData> value; +}; + +using ValueRange = QPair<NSUInteger, NSUInteger>; + +@interface QT_MANGLE_NAMESPACE(DarwinBTPeripheralManager) : NSObject<CBPeripheralManagerDelegate> + +- (id)initWith:(LECBManagerNotifier *)notifier; +- (void)dealloc; + +- (QSharedPointer<QLowEnergyServicePrivate>)addService:(const QLowEnergyServiceData &)data; +- (void) setParameters:(const QLowEnergyAdvertisingParameters &)parameters + data:(const QLowEnergyAdvertisingData &)data + scanResponse:(const QLowEnergyAdvertisingData &)scanResponse; + +// To be executed on the Qt's special BTLE dispatch queue. +- (void)startAdvertising; +- (void)stopAdvertising; +- (void)detach; + +- (void)write:(const QByteArray &)value + charHandle:(QLowEnergyHandle)charHandle; + + +// CBPeripheralManagerDelegate's callbacks (BTLE queue). +- (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral; +- (void)peripheralManager:(CBPeripheralManager *)peripheral + willRestoreState:(NSDictionary *)dict; +- (void)peripheralManagerDidStartAdvertising:(CBPeripheralManager *)peripheral + error:(NSError *)error; +- (void)peripheralManager:(CBPeripheralManager *)peripheral + didAddService:(CBService *)service error:(NSError *)error; +- (void)peripheralManager:(CBPeripheralManager *)peripheral central:(CBCentral *)central + didSubscribeToCharacteristic:(CBCharacteristic *)characteristic; +- (void)peripheralManager:(CBPeripheralManager *)peripheral central:(CBCentral *)central + didUnsubscribeFromCharacteristic:(CBCharacteristic *)characteristic; +- (void)peripheralManager:(CBPeripheralManager *)peripheral + didReceiveReadRequest:(CBATTRequest *)request; +- (void)peripheralManager:(CBPeripheralManager *)peripheral + didReceiveWriteRequests:(NSArray *)requests; +- (void)peripheralManagerIsReadyToUpdateSubscribers:(CBPeripheralManager *)peripheral; + +@end + +#endif diff --git a/src/bluetooth/darwin/btraii.mm b/src/bluetooth/darwin/btraii.mm new file mode 100644 index 00000000..a1bf2a8d --- /dev/null +++ b/src/bluetooth/darwin/btraii.mm @@ -0,0 +1,110 @@ +/**************************************************************************** +** +** Copyright (C) 2019 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 "btraii_p.h" + +#include <qdebug.h> + +#include <Foundation/Foundation.h> + +#include <utility> + +QT_BEGIN_NAMESPACE + +namespace DarwinBluetooth { + +StrongReference::StrongReference(void *object, RetainPolicy policy) + : objCInstance(object) +{ + if (policy == RetainPolicy::doInitialRetain) + objCInstance = [getAs<NSObject>() retain]; +} + +StrongReference::StrongReference(const StrongReference &other) +{ + objCInstance = [other.getAs<NSObject>() retain]; +} + +StrongReference::StrongReference(StrongReference &&other) +{ + std::swap(objCInstance, other.objCInstance); +} + +StrongReference::~StrongReference() +{ + [getAs<NSObject>() release]; +} + +StrongReference &StrongReference::operator = (const StrongReference &other) noexcept +{ + if (this != &other) { + [getAs<NSObject>() release]; + objCInstance = [other.getAs<NSObject>() retain]; + } + + return *this; +} + +StrongReference &StrongReference::operator = (StrongReference &&other) noexcept +{ + swap(other); + return *this; +} + +void StrongReference::reset() +{ + [getAs<NSObject>() release]; + objCInstance = nullptr; +} + +void StrongReference::reset(void *obj, RetainPolicy policy) +{ + [getAs<NSObject>() release]; + objCInstance = obj; + + if (policy == RetainPolicy::doInitialRetain) { + auto newInstance = static_cast<NSObject *>(obj); + Q_ASSERT(newInstance); + objCInstance = [newInstance retain]; + } +} + +} // namespace DarwinBluetooth + +QT_END_NAMESPACE diff --git a/src/bluetooth/darwin/btraii_p.h b/src/bluetooth/darwin/btraii_p.h new file mode 100644 index 00000000..6053d63b --- /dev/null +++ b/src/bluetooth/darwin/btraii_p.h @@ -0,0 +1,136 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtBluetooth module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef BTRAII_P_H +#define BTRAII_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/qglobal.h> + +#include <utility> + +QT_BEGIN_NAMESPACE + +namespace DarwinBluetooth { + +enum class RetainPolicy +{ + noInitialRetain, + doInitialRetain +}; + +// The class StrongReference and its descendant ScopedGuard +// are RAII classes dealing with raw pointers to NSObject class +// and its descendants (and thus hiding Objective-C's retain/ +// release semantics). The header itself is meant to be included +// into *.cpp files so it's a pure C++ code without any Objective-C +// syntax. Thus it's a bit clunky - the type information is 'erased' +// and has to be enforced by the code using these smart pointers. +// That's because these types are Objective-C classes - thus require +// Objective-C compiler to work. Member-function template 'getAs' is +// a convenience shortcut giving the desired pointer type in +// Objective-C++ files (*.mm). + +// TODO: on top of these classes I can build ObjCStrongReference (it's +// now inside osxbtutils_p.h, a template class that does have type +// information needed but works only in Objective-C++ environment. +class StrongReference +{ +public: + StrongReference() = default; + StrongReference(void *object, RetainPolicy policy); + StrongReference(const StrongReference &other); + StrongReference(StrongReference &&other); + + ~StrongReference(); + + StrongReference &operator = (const StrongReference &other) noexcept; + StrongReference &operator = (StrongReference &&other) noexcept; + + void swap(StrongReference &other) noexcept + { + std::swap(objCInstance, other.objCInstance); + } + + void reset(); + void reset(void *newInstance, RetainPolicy policy); + + template<class ObjCType> + ObjCType *getAs() const + { + return static_cast<ObjCType *>(objCInstance); + } + + operator bool() const + { + return !!objCInstance; + } + +private: + void *objCInstance = nullptr; +}; + +class ScopedPointer final : public StrongReference +{ +public: + ScopedPointer() = default; + ScopedPointer(void *instance, RetainPolicy policy) + : StrongReference(instance, policy) + { + } + +private: + Q_DISABLE_COPY_MOVE(ScopedPointer) +}; + +} // namespace DarwinBluetooth + +QT_END_NAMESPACE + +#endif // BTRAII_P_H diff --git a/src/bluetooth/darwin/btrfcommchannel.mm b/src/bluetooth/darwin/btrfcommchannel.mm new file mode 100644 index 00000000..0cac3c6d --- /dev/null +++ b/src/bluetooth/darwin/btrfcommchannel.mm @@ -0,0 +1,261 @@ +/**************************************************************************** +** +** 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 "btrfcommchannel_p.h" +#include "qbluetoothaddress.h" +#include "btdelegates_p.h" +#include "btutility_p.h" + +QT_USE_NAMESPACE + +@implementation QT_MANGLE_NAMESPACE(DarwinBTRFCOMMChannel) +{ + QT_PREPEND_NAMESPACE(DarwinBluetooth)::ChannelDelegate *delegate; + IOBluetoothDevice *device; + IOBluetoothRFCOMMChannel *channel; + bool connected; +} + +- (id)initWithDelegate:(DarwinBluetooth::ChannelDelegate *)aDelegate +{ + Q_ASSERT_X(aDelegate, Q_FUNC_INFO, "invalid delegate (null)"); + + if (self = [super init]) { + delegate = aDelegate; + device = nil; + channel = nil; + connected = false; + } + + return self; +} + +- (id)initWithDelegate:(QT_PREPEND_NAMESPACE(DarwinBluetooth::ChannelDelegate) *)aDelegate + channel:(IOBluetoothRFCOMMChannel *)aChannel +{ + // This type of channel does not require connect, it's created with + // already open channel. + Q_ASSERT_X(aDelegate, Q_FUNC_INFO, "invalid delegate (null)"); + Q_ASSERT_X(aChannel, Q_FUNC_INFO, "invalid channel (nil)"); + + if (self = [super init]) { + delegate = aDelegate; + channel = [aChannel retain]; + [channel setDelegate:self]; + device = [[channel getDevice] retain]; + connected = true; + } + + return self; +} + +- (void)dealloc +{ + if (channel) { + [channel setDelegate:nil]; + [channel closeChannel]; + [channel release]; + } + + [device release]; + + [super dealloc]; +} + +// A single async connection (you can not reuse this object). +- (IOReturn)connectAsyncToDevice:(const QBluetoothAddress &)address + withChannelID:(BluetoothRFCOMMChannelID)channelID +{ + if (address.isNull()) { + qCCritical(QT_BT_DARWIN) << "invalid peer address"; + return kIOReturnNoDevice; + } + + // Can never be called twice. + if (connected || device || channel) { + qCCritical(QT_BT_DARWIN) << "connection is already active"; + return kIOReturnStillOpen; + } + + QT_BT_MAC_AUTORELEASEPOOL; + + const BluetoothDeviceAddress iobtAddress = DarwinBluetooth::iobluetooth_address(address); + device = [IOBluetoothDevice deviceWithAddress:&iobtAddress]; + if (!device) { // TODO: do I always check this BTW??? Apple's docs say nothing about nil. + qCCritical(QT_BT_DARWIN) << "failed to create a device"; + return kIOReturnNoDevice; + } + + const IOReturn status = [device openRFCOMMChannelAsync:&channel + withChannelID:channelID delegate:self]; + if (status != kIOReturnSuccess) { + qCCritical(QT_BT_DARWIN) << "failed to open L2CAP channel"; + // device is still autoreleased. + device = nil; + return status; + } + + [channel retain];// What if we're closed already? + [device retain]; + + return kIOReturnSuccess; +} + +- (void)rfcommChannelData:(IOBluetoothRFCOMMChannel*)rfcommChannel + data:(void *)dataPointer length:(size_t)dataLength +{ + Q_UNUSED(rfcommChannel) + + Q_ASSERT_X(delegate, Q_FUNC_INFO, "invalid delegate (null)"); + + // Not sure if it can ever happen and if + // assert is better. + if (!dataPointer || !dataLength) + return; + + delegate->readChannelData(dataPointer, dataLength); +} + +- (void)rfcommChannelOpenComplete:(IOBluetoothRFCOMMChannel*)rfcommChannel + status:(IOReturn)error +{ + Q_UNUSED(rfcommChannel) + + Q_ASSERT_X(delegate, Q_FUNC_INFO, "invalid delegate (null)"); + + if (error != kIOReturnSuccess) { + delegate->setChannelError(error); + } else { + connected = true; + delegate->channelOpenComplete(); + } +} + +- (void)rfcommChannelClosed:(IOBluetoothRFCOMMChannel*)rfcommChannel +{ + Q_UNUSED(rfcommChannel) + + Q_ASSERT_X(delegate, Q_FUNC_INFO, "invalid delegate (null)"); + delegate->channelClosed(); + connected = false; +} + +- (void)rfcommChannelControlSignalsChanged:(IOBluetoothRFCOMMChannel*)rfcommChannel +{ + Q_UNUSED(rfcommChannel) +} + +- (void)rfcommChannelFlowControlChanged:(IOBluetoothRFCOMMChannel*)rfcommChannel +{ + Q_UNUSED(rfcommChannel) +} + +- (void)rfcommChannelWriteComplete:(IOBluetoothRFCOMMChannel*)rfcommChannel + refcon:(void*)refcon status:(IOReturn)error +{ + Q_UNUSED(rfcommChannel) + Q_UNUSED(refcon) + + Q_ASSERT_X(delegate, Q_FUNC_INFO, "invalid delegate (null)"); + + if (error != kIOReturnSuccess) + delegate->setChannelError(error); + else + delegate->writeComplete(); +} + +- (void)rfcommChannelQueueSpaceAvailable:(IOBluetoothRFCOMMChannel*)rfcommChannel +{ + Q_UNUSED(rfcommChannel) +} + +- (BluetoothRFCOMMChannelID)getChannelID +{ + if (channel) + return [channel getChannelID]; + + return 0; +} + +- (BluetoothDeviceAddress)peerAddress +{ + const BluetoothDeviceAddress *const addr = device ? [device getAddress] + : nullptr; + if (addr) + return *addr; + + return BluetoothDeviceAddress(); +} + +- (NSString *)peerName +{ + if (device) + return device.name; + + return nil; +} + +- (BluetoothRFCOMMMTU)getMTU +{ + if (channel) + return [channel getMTU]; + + return 0; +} + +- (IOReturn) writeSync:(void*)data length:(UInt16)length +{ + Q_ASSERT_X(data, Q_FUNC_INFO, "invalid data (null)"); + Q_ASSERT_X(length, Q_FUNC_INFO, "invalid data size"); + Q_ASSERT_X(connected && channel, Q_FUNC_INFO, "invalid RFCOMM channel"); + + return [channel writeSync:data length:length]; +} + +- (IOReturn) writeAsync:(void*)data length:(UInt16)length +{ + Q_ASSERT_X(data, Q_FUNC_INFO, "invalid data (null)"); + Q_ASSERT_X(length, Q_FUNC_INFO, "invalid data size"); + Q_ASSERT_X(connected && channel, Q_FUNC_INFO, "invalid RFCOMM channel"); + + return [channel writeAsync:data length:length refcon:nullptr]; +} + + +@end diff --git a/src/bluetooth/darwin/btrfcommchannel_p.h b/src/bluetooth/darwin/btrfcommchannel_p.h new file mode 100644 index 00000000..25299092 --- /dev/null +++ b/src/bluetooth/darwin/btrfcommchannel_p.h @@ -0,0 +1,108 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#ifndef BTRFCOMMCHANNEL_P_H +#define BTRFCOMMCHANNEL_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/qglobal.h> + +#include <Foundation/Foundation.h> + +#include <IOBluetooth/IOBluetooth.h> + +QT_BEGIN_NAMESPACE + +class QBluetoothAddress; + +namespace DarwinBluetooth { + +class ChannelDelegate; + +} + +QT_END_NAMESPACE + +@interface QT_MANGLE_NAMESPACE(DarwinBTRFCOMMChannel) : NSObject<IOBluetoothRFCOMMChannelDelegate> + +- (id)initWithDelegate:(QT_PREPEND_NAMESPACE(DarwinBluetooth)::ChannelDelegate *)aDelegate; +- (id)initWithDelegate:(QT_PREPEND_NAMESPACE(DarwinBluetooth)::ChannelDelegate *)aDelegate + channel:(IOBluetoothRFCOMMChannel *)aChannel; + +- (void)dealloc; + +// A single async connection (can connect only once). +- (IOReturn)connectAsyncToDevice:(const QT_PREPEND_NAMESPACE(QBluetoothAddress) &)address + withChannelID:(BluetoothRFCOMMChannelID)channelID; + +- (void)rfcommChannelData:(IOBluetoothRFCOMMChannel*)rfcommChannel + data:(void *)dataPointer length:(size_t)dataLength; +- (void)rfcommChannelOpenComplete:(IOBluetoothRFCOMMChannel*)rfcommChannel + status:(IOReturn)error; +- (void)rfcommChannelClosed:(IOBluetoothRFCOMMChannel*)rfcommChannel; +- (void)rfcommChannelControlSignalsChanged:(IOBluetoothRFCOMMChannel*)rfcommChannel; +- (void)rfcommChannelFlowControlChanged:(IOBluetoothRFCOMMChannel*)rfcommChannel; +- (void)rfcommChannelWriteComplete:(IOBluetoothRFCOMMChannel*)rfcommChannel + refcon:(void*)refcon status:(IOReturn)error; +- (void)rfcommChannelQueueSpaceAvailable:(IOBluetoothRFCOMMChannel*)rfcommChannel; + +// +- (BluetoothRFCOMMChannelID)getChannelID; +- (BluetoothDeviceAddress)peerAddress; +- (NSString *)peerName; + +- (BluetoothRFCOMMMTU)getMTU; + +- (IOReturn) writeSync:(void*)data length:(UInt16)length; +- (IOReturn) writeAsync:(void*)data length:(UInt16)length; + + +@end + +#endif diff --git a/src/bluetooth/darwin/btsdpinquiry.mm b/src/bluetooth/darwin/btsdpinquiry.mm new file mode 100644 index 00000000..336d7541 --- /dev/null +++ b/src/bluetooth/darwin/btsdpinquiry.mm @@ -0,0 +1,329 @@ +/**************************************************************************** +** +** 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 "qbluetoothserviceinfo.h" +#include "btsdpinquiry_p.h" +#include "qbluetoothuuid.h" +#include "btdelegates_p.h" +#include "btutility_p.h" + +#include <QtCore/qvariant.h> +#include <QtCore/qstring.h> + +QT_BEGIN_NAMESPACE + +namespace DarwinBluetooth { + +namespace { + +QBluetoothUuid sdp_element_to_uuid(IOBluetoothSDPDataElement *element) +{ + QT_BT_MAC_AUTORELEASEPOOL; + + if (!element || [element getTypeDescriptor] != kBluetoothSDPDataElementTypeUUID) + return {}; + + return qt_uuid([[element getUUIDValue] getUUIDWithLength:16]); +} + +QBluetoothUuid extract_service_ID(IOBluetoothSDPServiceRecord *record) +{ + Q_ASSERT(record); + + QT_BT_MAC_AUTORELEASEPOOL; + + return sdp_element_to_uuid([record getAttributeDataElement:kBluetoothSDPAttributeIdentifierServiceID]); +} + +QVector<QBluetoothUuid> extract_service_class_ID_list(IOBluetoothSDPServiceRecord *record) +{ + Q_ASSERT(record); + + QT_BT_MAC_AUTORELEASEPOOL; + + IOBluetoothSDPDataElement *const idList = [record getAttributeDataElement:kBluetoothSDPAttributeIdentifierServiceClassIDList]; + if (!idList || [idList getTypeDescriptor] != kBluetoothSDPDataElementTypeDataElementSequence) + return {}; + + QVector<QBluetoothUuid> uuids; + NSArray *const arr = [idList getArrayValue]; + for (IOBluetoothSDPDataElement *dataElement in arr) { + const auto qtUuid = sdp_element_to_uuid(dataElement); + if (!qtUuid.isNull()) + uuids.push_back(qtUuid); + } + + return uuids; +} + +QBluetoothServiceInfo::Sequence service_class_ID_list_to_sequence(const QVector<QBluetoothUuid> &uuids) +{ + if (uuids.isEmpty()) + return {}; + + QBluetoothServiceInfo::Sequence sequence; + for (const auto &uuid : uuids) { + Q_ASSERT(!uuid.isNull()); + sequence.append(QVariant::fromValue(uuid)); + } + + return sequence; +} + +} // unnamed namespace + +QVariant extract_attribute_value(IOBluetoothSDPDataElement *dataElement) +{ + Q_ASSERT_X(dataElement, Q_FUNC_INFO, "invalid data element (nil)"); + + // TODO: error handling and diagnostic messages. + + // All "temporary" obj-c objects are autoreleased. + QT_BT_MAC_AUTORELEASEPOOL; + + const BluetoothSDPDataElementTypeDescriptor typeDescriptor = [dataElement getTypeDescriptor]; + + switch (typeDescriptor) { + case kBluetoothSDPDataElementTypeNil: + break; + case kBluetoothSDPDataElementTypeUnsignedInt: + return [[dataElement getNumberValue] unsignedIntValue]; + case kBluetoothSDPDataElementTypeSignedInt: + return [[dataElement getNumberValue] intValue]; + case kBluetoothSDPDataElementTypeUUID: + return QVariant::fromValue(sdp_element_to_uuid(dataElement)); + case kBluetoothSDPDataElementTypeString: + case kBluetoothSDPDataElementTypeURL: + return QString::fromNSString([dataElement getStringValue]); + case kBluetoothSDPDataElementTypeBoolean: + return [[dataElement getNumberValue] boolValue]; + case kBluetoothSDPDataElementTypeDataElementSequence: + case kBluetoothSDPDataElementTypeDataElementAlternative: // TODO: check this! + { + QBluetoothServiceInfo::Sequence sequence; + NSArray *const arr = [dataElement getArrayValue]; + for (IOBluetoothSDPDataElement *element in arr) + sequence.append(extract_attribute_value(element)); + + return QVariant::fromValue(sequence); + } + break;// Coding style. + default:; + } + + return QVariant(); +} + +void extract_service_record(IOBluetoothSDPServiceRecord *record, QBluetoothServiceInfo &serviceInfo) +{ + QT_BT_MAC_AUTORELEASEPOOL; + + if (!record) + return; + + NSDictionary *const attributes = record.attributes; + NSEnumerator *const keys = attributes.keyEnumerator; + for (NSNumber *key in keys) { + const quint16 attributeID = [key unsignedShortValue]; + IOBluetoothSDPDataElement *const element = [attributes objectForKey:key]; + const QVariant attributeValue = DarwinBluetooth::extract_attribute_value(element); + serviceInfo.setAttribute(attributeID, attributeValue); + } + + const QBluetoothUuid serviceUuid = extract_service_ID(record); + if (!serviceUuid.isNull()) + serviceInfo.setServiceUuid(serviceUuid); + + const QVector<QBluetoothUuid> uuids(extract_service_class_ID_list(record)); + const auto sequence = service_class_ID_list_to_sequence(uuids); + if (!sequence.isEmpty()) + serviceInfo.setAttribute(QBluetoothServiceInfo::ServiceClassIds, sequence); +} + +QVector<QBluetoothUuid> extract_services_uuids(IOBluetoothDevice *device) +{ + QVector<QBluetoothUuid> uuids; + + // All "temporary" obj-c objects are autoreleased. + QT_BT_MAC_AUTORELEASEPOOL; + + if (!device || !device.services) + return uuids; + + NSArray * const records = device.services; + for (IOBluetoothSDPServiceRecord *record in records) { + const QBluetoothUuid serviceID = extract_service_ID(record); + if (!serviceID.isNull()) + uuids.push_back(serviceID); + + const QVector<QBluetoothUuid> idList(extract_service_class_ID_list(record)); + if (idList.size()) + uuids.append(idList); + } + + return uuids; +} + +} // namespace DarwinBluetooth + +QT_END_NAMESPACE + +QT_USE_NAMESPACE + +using namespace DarwinBluetooth; + +@implementation QT_MANGLE_NAMESPACE(DarwinBTSDPInquiry) +{ + QT_PREPEND_NAMESPACE(DarwinBluetooth::SDPInquiryDelegate) *delegate; + IOBluetoothDevice *device; + bool isActive; +} + +- (id)initWithDelegate:(DarwinBluetooth::SDPInquiryDelegate *)aDelegate +{ + Q_ASSERT_X(aDelegate, Q_FUNC_INFO, "invalid delegate (null)"); + + if (self = [super init]) { + delegate = aDelegate; + isActive = false; + } + + return self; +} + +- (void)dealloc +{ + //[device closeConnection]; //??? - synchronous, "In the future this API will be changed to allow asynchronous operation." + [device release]; + [super dealloc]; +} + +- (IOReturn)performSDPQueryWithDevice:(const QBluetoothAddress &)address +{ + Q_ASSERT_X(!isActive, Q_FUNC_INFO, "SDP query in progress"); + + QList<QBluetoothUuid> emptyFilter; + return [self performSDPQueryWithDevice:address filters:emptyFilter]; +} + +- (IOReturn)performSDPQueryWithDevice:(const QBluetoothAddress &)address + filters:(const QList<QBluetoothUuid> &)qtFilters +{ + Q_ASSERT_X(!isActive, Q_FUNC_INFO, "SDP query in progress"); + Q_ASSERT_X(!address.isNull(), Q_FUNC_INFO, "invalid target device address"); + + QT_BT_MAC_AUTORELEASEPOOL; + + // We first try to allocate "filters": + ObjCScopedPointer<NSMutableArray> array; + if (qtFilters.size()) { + array.reset([[NSMutableArray alloc] init]); + if (!array) { + qCCritical(QT_BT_DARWIN) << "failed to allocate an uuid filter"; + return kIOReturnError; + } + + for (const QBluetoothUuid &qUuid : qtFilters) { + ObjCStrongReference<IOBluetoothSDPUUID> uuid(iobluetooth_uuid(qUuid)); + if (uuid) + [array addObject:uuid]; + } + + if (int([array count]) != qtFilters.size()) { + qCCritical(QT_BT_DARWIN) << "failed to create an uuid filter"; + return kIOReturnError; + } + } + + const BluetoothDeviceAddress iobtAddress(iobluetooth_address(address)); + ObjCScopedPointer<IOBluetoothDevice> newDevice([[IOBluetoothDevice deviceWithAddress:&iobtAddress] retain]); + if (!newDevice) { + qCCritical(QT_BT_DARWIN) << "failed to create an IOBluetoothDevice object"; + return kIOReturnError; + } + + ObjCScopedPointer<IOBluetoothDevice> oldDevice(device); + device = newDevice.data(); + + IOReturn result = kIOReturnSuccess; + if (qtFilters.size()) + result = [device performSDPQuery:self uuids:array]; + else + result = [device performSDPQuery:self]; + + if (result != kIOReturnSuccess) { + qCCritical(QT_BT_DARWIN) << "failed to start an SDP query"; + device = oldDevice.take(); + } else { + isActive = true; + newDevice.take(); + } + + return result; +} + +- (void)stopSDPQuery +{ + // There is no API to stop it, + // but there is a 'stop' member-function in Qt and + // after it's called sdpQueryComplete must be somehow ignored. + + [device release]; + device = nil; +} + +- (void)sdpQueryComplete:(IOBluetoothDevice *)aDevice status:(IOReturn)status +{ + // Can happen - there is no legal way to cancel an SDP query, + // after the 'reset' device can never be + // the same as the cancelled one. + if (device != aDevice) + return; + + Q_ASSERT_X(delegate, Q_FUNC_INFO, "invalid delegate (null)"); + + isActive = false; + + if (status != kIOReturnSuccess) + delegate->SDPInquiryError(aDevice, status); + else + delegate->SDPInquiryFinished(aDevice); +} + +@end diff --git a/src/bluetooth/darwin/btsdpinquiry_p.h b/src/bluetooth/darwin/btsdpinquiry_p.h new file mode 100644 index 00000000..6cce5703 --- /dev/null +++ b/src/bluetooth/darwin/btsdpinquiry_p.h @@ -0,0 +1,99 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#ifndef BTSDPINQUIRY_H +#define BTSDPINQUIRY_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qbluetoothaddress.h" +#include "qbluetoothuuid.h" + +#include <QtCore/qglobal.h> +#include <QtCore/qlist.h> +#include <QtCore/qvector.h> + +#include <Foundation/Foundation.h> + +#include <IOBluetooth/IOBluetooth.h> + +@class QT_MANGLE_NAMESPACE(DarwinBTSDPInquiry); + +QT_BEGIN_NAMESPACE + +class QBluetoothServiceInfo; +class QVariant; + +namespace DarwinBluetooth { + +class SDPInquiryDelegate; + +void extract_service_record(IOBluetoothSDPServiceRecord *record, QBluetoothServiceInfo &serviceInfo); +QVariant extract_attribute_value(IOBluetoothSDPDataElement *dataElement); +QVector<QBluetoothUuid> extract_services_uuids(IOBluetoothDevice *device); + +} // namespace DarwinBluetooth + +QT_END_NAMESPACE + +@interface QT_MANGLE_NAMESPACE(DarwinBTSDPInquiry) : NSObject + +- (id)initWithDelegate:(QT_PREPEND_NAMESPACE(DarwinBluetooth::SDPInquiryDelegate) *)aDelegate; +- (void)dealloc; + +- (IOReturn)performSDPQueryWithDevice:(const QBluetoothAddress &)address; +- (IOReturn)performSDPQueryWithDevice:(const QBluetoothAddress &)address + filters:(const QList<QBluetoothUuid> &)filters; + +- (void)stopSDPQuery; + +- (void)sdpQueryComplete:(IOBluetoothDevice *)aDevice status:(IOReturn)status; + +@end + +#endif diff --git a/src/bluetooth/darwin/btservicerecord.mm b/src/bluetooth/darwin/btservicerecord.mm new file mode 100644 index 00000000..82067558 --- /dev/null +++ b/src/bluetooth/darwin/btservicerecord.mm @@ -0,0 +1,458 @@ +/**************************************************************************** +** +** 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 "qbluetoothserviceinfo.h" +#include "btservicerecord_p.h" + +#include <QtCore/qvariant.h> +#include <QtCore/qdebug.h> +#include <QtCore/qmap.h> +#include <QtCore/qurl.h> + +#include <IOBluetooth/IOBluetooth.h> + +QT_BEGIN_NAMESPACE + +namespace DarwinBluetooth { + +// +// Returns a dictionary containing the Bluetooth RFCOMM service definition +// corresponding to the provided |uuid| and |options|. +namespace { + +typedef ObjCStrongReference<NSMutableDictionary> Dictionary; +typedef ObjCStrongReference<IOBluetoothSDPUUID> SDPUUid; +typedef ObjCStrongReference<NSNumber> Number; +typedef QBluetoothServiceInfo QSInfo; +typedef QSInfo::Sequence Sequence; +typedef QSInfo::AttributeId AttributeId; + +} + +#if 0 +QBluetoothUuid profile_uuid(const QBluetoothServiceInfo &serviceInfo) +{ + // Strategy to pick service uuid: + // 1.) use serviceUuid() + // 2.) use first custom uuid if available + // 3.) use first service class uuid + QBluetoothUuid serviceUuid(serviceInfo.serviceUuid()); + + if (serviceUuid.isNull()) { + const QVariant var(serviceInfo.attribute(QBluetoothServiceInfo::ServiceClassIds)); + if (var.isValid()) { + const Sequence seq(var.value<Sequence>()); + + for (int i = 0; i < seq.count(); ++i) { + QBluetoothUuid uuid(seq.at(i).value<QBluetoothUuid>()); + if (uuid.isNull()) + continue; + + const int size = uuid.minimumSize(); + if (size == 2 || size == 4) { // Base UUID derived + if (serviceUuid.isNull()) + serviceUuid = uuid; + } else { + return uuid; + } + } + } + } + + return serviceUuid; +} +#endif + +template<class IntType> +Number variant_to_nsnumber(const QVariant &); + +template<> +Number variant_to_nsnumber<unsigned char>(const QVariant &var) +{ + return Number([NSNumber numberWithUnsignedChar:var.value<unsigned char>()], true); +} + +template<> +Number variant_to_nsnumber<unsigned short>(const QVariant &var) +{ + return Number([NSNumber numberWithUnsignedShort:var.value<unsigned short>()], true); +} + +template<> +Number variant_to_nsnumber<unsigned>(const QVariant &var) +{ + return Number([NSNumber numberWithUnsignedInt:var.value<unsigned>()], true); +} + +template<> +Number variant_to_nsnumber<char>(const QVariant &var) +{ + return Number([NSNumber numberWithChar:var.value<char>()], true); +} + +template<> +Number variant_to_nsnumber<short>(const QVariant &var) +{ + return Number([NSNumber numberWithShort:var.value<short>()], true); +} + +template<> +Number variant_to_nsnumber<int>(const QVariant &var) +{ + return Number([NSNumber numberWithInt:var.value<int>()], true); +} + +template<class ValueType> +void add_attribute(const QVariant &var, AttributeId key, Dictionary dict) +{ + Q_ASSERT_X(dict, Q_FUNC_INFO, "invalid dictionary (nil)"); + + if (!var.canConvert<ValueType>()) + return; + + const Number num(variant_to_nsnumber<ValueType>(var)); + [dict setObject:num forKey:[NSString stringWithFormat:@"%d", int(key)]]; +} + +template<> +void add_attribute<QString>(const QVariant &var, AttributeId key, Dictionary dict) +{ + Q_ASSERT_X(dict, Q_FUNC_INFO, "invalid dictionary (nil)"); + + if (!var.canConvert<QString>()) + return; + + const QString string(var.value<QString>()); + if (string.length()) { + if (NSString *const nsString = string.toNSString()) + [dict setObject:nsString forKey:[NSString stringWithFormat:@"%d", int(key)]]; + } +} + +template<> +void add_attribute<QBluetoothUuid>(const QVariant &var, AttributeId key, Dictionary dict) +{ + Q_ASSERT_X(dict, Q_FUNC_INFO, "invalid dictionary (nil)"); + + if (!var.canConvert<QBluetoothUuid>()) + return; + + SDPUUid ioUUID(iobluetooth_uuid(var.value<QBluetoothUuid>())); + [dict setObject:ioUUID forKey:[NSString stringWithFormat:@"%d", int(key)]]; +} + +template<> +void add_attribute<QUrl>(const QVariant &var, AttributeId key, Dictionary dict) +{ + Q_ASSERT_X(dict, Q_FUNC_INFO, "invalid dictionary (nil)"); + + if (!var.canConvert<QUrl>()) + return; + + Q_UNUSED(var) + Q_UNUSED(key) + Q_UNUSED(dict) + + // TODO: not clear how should I pass an url in a dictionary, NSURL does not work. +} + +template<class ValueType> +void add_attribute(const QVariant &var, NSMutableArray *list); + +template<class ValueType> +void add_attribute(const QVariant &var, NSMutableArray *list) +{ + Q_ASSERT_X(list, Q_FUNC_INFO, "invalid list (nil)"); + + if (!var.canConvert<ValueType>()) + return; + + const Number num(variant_to_nsnumber<ValueType>(var)); + [list addObject:num]; +} + +template<> +void add_attribute<QString>(const QVariant &var, NSMutableArray *list) +{ + Q_ASSERT_X(list, Q_FUNC_INFO, "invalid list (nil)"); + + if (!var.canConvert<QString>()) + return; + + const QString string(var.value<QString>()); + if (string.length()) { + if (NSString *const nsString = string.toNSString()) + [list addObject:nsString]; + } +} + +template<> +void add_attribute<QBluetoothUuid>(const QVariant &var, NSMutableArray *list) +{ + Q_ASSERT_X(list, Q_FUNC_INFO, "invalid list (nil)"); + + if (!var.canConvert<QBluetoothUuid>()) + return; + + SDPUUid ioUUID(iobluetooth_uuid(var.value<QBluetoothUuid>())); + [list addObject:ioUUID]; +} + +template<> +void add_attribute<QUrl>(const QVariant &var, NSMutableArray *list) +{ + Q_ASSERT_X(list, Q_FUNC_INFO, "invalid list (nil)"); + + if (!var.canConvert<QUrl>()) + return; + + Q_UNUSED(var) + Q_UNUSED(list) + // TODO: not clear how should I pass an url in a dictionary, NSURL does not work. +} + +void add_rfcomm_protocol_descriptor_list(uint16 channelID, Dictionary dict) +{ + Q_ASSERT_X(dict, Q_FUNC_INFO, "invalid dictionary (nil)"); + + QT_BT_MAC_AUTORELEASEPOOL; + + // Objective-C has literals (for arrays and dictionaries), but it will not compile + // on 10.7 or below, so quite a lot of code here. + + NSMutableArray *const descriptorList = [NSMutableArray array]; + + IOBluetoothSDPUUID *const l2capUUID = [IOBluetoothSDPUUID uuid16:kBluetoothSDPUUID16L2CAP]; + NSArray *const l2capList = [NSArray arrayWithObject:l2capUUID]; + + [descriptorList addObject:l2capList]; + // + IOBluetoothSDPUUID *const rfcommUUID = [IOBluetoothSDPUUID uuid16:kBluetoothSDPUUID16RFCOMM]; + NSMutableDictionary *const rfcommDict = [NSMutableDictionary dictionary]; + [rfcommDict setObject:[NSNumber numberWithInt:1] forKey:@"DataElementType"]; + [rfcommDict setObject:[NSNumber numberWithInt:1] forKey:@"DataElementSize"]; + [rfcommDict setObject:[NSNumber numberWithInt:channelID] forKey:@"DataElementValue"]; + // + NSMutableArray *const rfcommList = [NSMutableArray array]; + [rfcommList addObject:rfcommUUID]; + [rfcommList addObject:rfcommDict]; + + [descriptorList addObject:rfcommList]; + [dict setObject:descriptorList forKey:[NSString stringWithFormat:@"%d", + kBluetoothSDPAttributeIdentifierProtocolDescriptorList]]; +} + +void add_l2cap_protocol_descriptor_list(uint16 psm, Dictionary dict) +{ + Q_ASSERT_X(dict, Q_FUNC_INFO, "invalid dictionary (nil)"); + + QT_BT_MAC_AUTORELEASEPOOL; + + // Objective-C has literals (for arrays and dictionaries), but it will not compile + // on 10.7 or below, so quite a lot of code here. + + NSMutableArray *const descriptorList = [NSMutableArray array]; + NSMutableArray *const l2capList = [NSMutableArray array]; + + IOBluetoothSDPUUID *const l2capUUID = [IOBluetoothSDPUUID uuid16:kBluetoothSDPUUID16L2CAP]; + [l2capList addObject:l2capUUID]; + + NSMutableDictionary *const l2capDict = [NSMutableDictionary dictionary]; + [l2capDict setObject:[NSNumber numberWithInt:1] forKey:@"DataElementType"]; + [l2capDict setObject:[NSNumber numberWithInt:2] forKey:@"DataElementSize"]; + [l2capDict setObject:[NSNumber numberWithInt:psm] forKey:@"DataElementValue"]; + [l2capList addObject:l2capDict]; + + [descriptorList addObject:l2capList]; + [dict setObject:descriptorList forKey:[NSString stringWithFormat:@"%d", + kBluetoothSDPAttributeIdentifierProtocolDescriptorList]]; +} + +bool add_attribute(const QVariant &var, AttributeId key, NSMutableArray *list) +{ + Q_ASSERT_X(list, Q_FUNC_INFO, "invalid list (nil)"); + + if (var.canConvert<Sequence>()) + return false; + + if (var.canConvert<QString>()) { + //ServiceName, ServiceDescription, ServiceProvider. + add_attribute<QString>(var, list); + } else if (var.canConvert<QBluetoothUuid>()) { + add_attribute<QBluetoothUuid>(var, list); + } else { + // Here we need 'key' to understand the type. + // We can have different integer types actually, so I have to check + // the 'key' to be sure the conversion is reasonable. + switch (key) { + case QSInfo::ServiceRecordHandle: + case QSInfo::ServiceRecordState: + case QSInfo::ServiceInfoTimeToLive: + add_attribute<unsigned>(var, list); + break; + case QSInfo::ServiceAvailability: + add_attribute<unsigned char>(var, list); + break; + case QSInfo::IconUrl: + case QSInfo::DocumentationUrl: + case QSInfo::ClientExecutableUrl: + add_attribute<QUrl>(var, list); + break; + default:; + } + } + + return true; +} + +bool add_attribute(const QBluetoothServiceInfo &serviceInfo, AttributeId key, Dictionary dict) +{ + Q_ASSERT_X(dict, Q_FUNC_INFO, "invalid dict (nil)"); + + const QVariant var(serviceInfo.attribute(key)); + if (var.canConvert<Sequence>()) + return false; + + if (var.canConvert<QString>()) { + //ServiceName, ServiceDescription, ServiceProvider. + add_attribute<QString>(var, key, dict); + } else if (var.canConvert<QBluetoothUuid>()) { + add_attribute<QBluetoothUuid>(serviceInfo.attribute(key), key, dict); + } else { + // We can have different integer types actually, so I have to check + // the 'key' to be sure the conversion is reasonable. + switch (key) { + case QSInfo::ServiceRecordHandle: + case QSInfo::ServiceRecordState: + case QSInfo::ServiceInfoTimeToLive: + add_attribute<unsigned>(serviceInfo.attribute(key), key, dict); + break; + case QSInfo::ServiceAvailability: + add_attribute<unsigned char>(serviceInfo.attribute(key), key, dict); + break; + case QSInfo::IconUrl: + case QSInfo::DocumentationUrl: + case QSInfo::ClientExecutableUrl: + add_attribute<QUrl>(serviceInfo.attribute(key), key, dict); + break; + default:; + } + } + + return true; +} + +bool add_sequence_attribute(const QVariant &var, AttributeId key, NSMutableArray *list) +{ + // Add a "nested" sequence. + Q_ASSERT_X(list, Q_FUNC_INFO, "invalid list (nil)"); + + if (var.isNull() || !var.canConvert<Sequence>()) + return false; + + const Sequence sequence(var.value<Sequence>()); + for (const QVariant &var : sequence) { + if (var.canConvert<Sequence>()) { + NSMutableArray *const nested = [NSMutableArray array]; + add_sequence_attribute(var, key, nested); + [list addObject:nested]; + } else { + add_attribute(var, key, list); + } + } + + return true; +} + +bool add_sequence_attribute(const QBluetoothServiceInfo &serviceInfo, AttributeId key, Dictionary dict) +{ + Q_ASSERT_X(dict, Q_FUNC_INFO, "invalid dictionary (nil)"); + + const QVariant &var(serviceInfo.attribute(key)); + if (var.isNull() || !var.canConvert<Sequence>()) + return false; + + QT_BT_MAC_AUTORELEASEPOOL; + + NSMutableArray *const list = [NSMutableArray array]; + const Sequence sequence(var.value<Sequence>()); + for (const QVariant &element : sequence) { + if (!add_sequence_attribute(element, key, list)) + add_attribute(element, key, list); + } + [dict setObject:list forKey:[NSString stringWithFormat:@"%d", int(key)]]; + + return true; +} + +Dictionary iobluetooth_service_dictionary(const QBluetoothServiceInfo &serviceInfo) +{ + Dictionary dict; + + if (serviceInfo.socketProtocol() == QBluetoothServiceInfo::UnknownProtocol) + return dict; + + const QList<quint16> attributeIds(serviceInfo.attributes()); + if (!attributeIds.size()) + return dict; + + dict.reset([[NSMutableDictionary alloc] init]); + + for (quint16 key : attributeIds) { + if (key == QSInfo::ProtocolDescriptorList) // We handle it in a special way. + continue; + // TODO: check if non-sequence QVariant still must be + // converted into NSArray for some attribute ID. + if (!add_sequence_attribute(serviceInfo, AttributeId(key), dict)) + add_attribute(serviceInfo, AttributeId(key), dict); + } + + if (serviceInfo.socketProtocol() == QBluetoothServiceInfo::L2capProtocol) { + add_l2cap_protocol_descriptor_list(serviceInfo.protocolServiceMultiplexer(), + dict); + } else { + add_rfcomm_protocol_descriptor_list(serviceInfo.serverChannel(), dict); + } + + return dict; +} + +} + +QT_END_NAMESPACE diff --git a/src/bluetooth/darwin/btservicerecord_p.h b/src/bluetooth/darwin/btservicerecord_p.h new file mode 100644 index 00000000..6b5b0374 --- /dev/null +++ b/src/bluetooth/darwin/btservicerecord_p.h @@ -0,0 +1,72 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#ifndef BTSERVICERECORD_P_H +#define BTSERVICERECORD_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "btutility_p.h" + +#include <QtCore/qglobal.h> + +#include <Foundation/Foundation.h> + +QT_BEGIN_NAMESPACE + +class QBluetoothServiceInfo; + +namespace DarwinBluetooth { + +ObjCStrongReference<NSMutableDictionary> iobluetooth_service_dictionary(const QBluetoothServiceInfo &serviceInfo); + +} // namespace DarwinBluetooth + +QT_END_NAMESPACE + +#endif diff --git a/src/bluetooth/darwin/btsocketlistener.mm b/src/bluetooth/darwin/btsocketlistener.mm new file mode 100644 index 00000000..505fec14 --- /dev/null +++ b/src/bluetooth/darwin/btsocketlistener.mm @@ -0,0 +1,128 @@ +/**************************************************************************** +** +** 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 "btsocketlistener_p.h" +#include "btdelegates_p.h" +#include "btutility_p.h" + +#include <QtCore/qdebug.h> + +QT_USE_NAMESPACE + +@implementation QT_MANGLE_NAMESPACE(DarwinBTSocketListener) +{ + IOBluetoothUserNotification *connectionNotification; + QT_PREPEND_NAMESPACE(DarwinBluetooth::SocketListener) *delegate; + quint16 port; +} + +- (id)initWithListener:(DarwinBluetooth::SocketListener *)aDelegate +{ + Q_ASSERT_X(aDelegate, Q_FUNC_INFO, "invalid delegate (null)"); + if (self = [super init]) { + connectionNotification = nil; + delegate = aDelegate; + port = 0; + } + + return self; +} + +- (void)dealloc +{ + [connectionNotification unregister]; + [connectionNotification release]; + + [super dealloc]; +} + +- (bool)listenRFCOMMConnectionsWithChannelID:(BluetoothRFCOMMChannelID)channelID +{ + Q_ASSERT_X(!connectionNotification, Q_FUNC_INFO, "already listening"); + + connectionNotification = [IOBluetoothRFCOMMChannel registerForChannelOpenNotifications:self + selector:@selector(rfcommOpenNotification:channel:) + withChannelID:channelID + direction:kIOBluetoothUserNotificationChannelDirectionIncoming]; + connectionNotification = [connectionNotification retain]; + if (connectionNotification) + port = channelID; + + return connectionNotification; +} + +- (bool)listenL2CAPConnectionsWithPSM:(BluetoothL2CAPPSM)psm +{ + Q_ASSERT_X(!connectionNotification, Q_FUNC_INFO, "already listening"); + + connectionNotification = [IOBluetoothL2CAPChannel registerForChannelOpenNotifications:self + selector:@selector(l2capOpenNotification:channel:) + withPSM:psm + direction:kIOBluetoothUserNotificationChannelDirectionIncoming]; + connectionNotification = [connectionNotification retain]; + if (connectionNotification) + port = psm; + + return connectionNotification; +} + +- (void)rfcommOpenNotification:(IOBluetoothUserNotification *)notification + channel:(IOBluetoothRFCOMMChannel *)newChannel +{ + Q_UNUSED(notification) + + Q_ASSERT_X(delegate, Q_FUNC_INFO, "invalid delegate (null)"); + delegate->openNotifyRFCOMM(newChannel); +} + +- (void)l2capOpenNotification:(IOBluetoothUserNotification *)notification + channel:(IOBluetoothL2CAPChannel *)newChannel +{ + Q_UNUSED(notification) + + Q_ASSERT_X(delegate, Q_FUNC_INFO, "invalid delegate (null)"); + delegate->openNotifyL2CAP(newChannel); +} + +- (quint16)port +{ + return port; +} + +@end diff --git a/src/bluetooth/darwin/btsocketlistener_p.h b/src/bluetooth/darwin/btsocketlistener_p.h new file mode 100644 index 00000000..4b9b267a --- /dev/null +++ b/src/bluetooth/darwin/btsocketlistener_p.h @@ -0,0 +1,91 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#ifndef BTSOCKETLISTENER_P_H +#define BTSOCKETLISTENER_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/qglobal.h> + +#include <Foundation/Foundation.h> + +#include <IOBluetooth/IOBluetooth.h> + +QT_BEGIN_NAMESPACE + +namespace DarwinBluetooth { + +class SocketListener; + +} + +QT_END_NAMESPACE + +// A single DarwinBTSocketListener can be started only once with +// RFCOMM or L2CAP protocol. It must be deleted to stop listening. + +@interface QT_MANGLE_NAMESPACE(DarwinBTSocketListener) : NSObject + +- (id)initWithListener:(QT_PREPEND_NAMESPACE(DarwinBluetooth::SocketListener) *)aDelegate; +- (void)dealloc; + +- (bool)listenRFCOMMConnectionsWithChannelID:(BluetoothRFCOMMChannelID)channelID; +- (bool)listenL2CAPConnectionsWithPSM:(BluetoothL2CAPPSM)psm; + +- (void)rfcommOpenNotification:(IOBluetoothUserNotification *)notification + channel:(IOBluetoothRFCOMMChannel *)newChannel; + +- (void)l2capOpenNotification:(IOBluetoothUserNotification *)notification + channel:(IOBluetoothL2CAPChannel *)newChannel; + +- (quint16)port; + +@end + +#endif diff --git a/src/bluetooth/darwin/btutility.mm b/src/bluetooth/darwin/btutility.mm new file mode 100644 index 00000000..6f223a82 --- /dev/null +++ b/src/bluetooth/darwin/btutility.mm @@ -0,0 +1,372 @@ +/**************************************************************************** +** +** 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 "qlowenergycharacteristicdata.h" +#include "qbluetoothaddress.h" +#include "btutility_p.h" +#include "qbluetoothuuid.h" + +#include <QtCore/qendian.h> +#include <QtCore/qstring.h> + +#ifndef QT_IOS_BLUETOOTH + +#import <IOBluetooth/objc/IOBluetoothSDPUUID.h> +#import <CoreFoundation/CoreFoundation.h> +#import <CoreBluetooth/CBUUID.h> + +#endif + +#include <algorithm> +#include <limits> + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(QT_BT_DARWIN, "qt.bluetooth.darwin") + +namespace DarwinBluetooth { + +const int defaultLEScanTimeoutMS = 25000; +// We use it only on iOS for now: +const int maxValueLength = 512; + +QString qt_address(NSString *address) +{ + if (address && address.length) { + NSString *const fixed = [address stringByReplacingOccurrencesOfString:@"-" withString:@":"]; + return QString::fromNSString(fixed); + } + + return QString(); +} + +#ifndef QT_IOS_BLUETOOTH + + +QBluetoothAddress qt_address(const BluetoothDeviceAddress *a) +{ + if (a) { + // TODO: can a byte order be different in BluetoothDeviceAddress? + const quint64 qAddress = a->data[5] | + qint64(a->data[4]) << 8 | + qint64(a->data[3]) << 16 | + qint64(a->data[2]) << 24 | + qint64(a->data[1]) << 32 | + qint64(a->data[0]) << 40; + return QBluetoothAddress(qAddress); + } + + return QBluetoothAddress(); +} + +BluetoothDeviceAddress iobluetooth_address(const QBluetoothAddress &qAddress) +{ + BluetoothDeviceAddress a = {}; + if (!qAddress.isNull()) { + const quint64 val = qAddress.toUInt64(); + a.data[0] = (val >> 40) & 0xff; + a.data[1] = (val >> 32) & 0xff; + a.data[2] = (val >> 24) & 0xff; + a.data[3] = (val >> 16) & 0xff; + a.data[4] = (val >> 8) & 0xff; + a.data[5] = val & 0xff; + } + + return a; +} + +ObjCStrongReference<IOBluetoothSDPUUID> iobluetooth_uuid(const QBluetoothUuid &uuid) +{ + const unsigned nBytes = 128 / std::numeric_limits<unsigned char>::digits; + const quint128 intVal(uuid.toUInt128()); + + const ObjCStrongReference<IOBluetoothSDPUUID> iobtUUID([IOBluetoothSDPUUID uuidWithBytes:intVal.data + length:nBytes], true); + return iobtUUID; +} + +QBluetoothUuid qt_uuid(IOBluetoothSDPUUID *uuid) +{ + QBluetoothUuid qtUuid; + if (!uuid || [uuid length] != 16) // TODO: issue any diagnostic? + return qtUuid; + + // TODO: ensure the correct byte-order!!! + quint128 uuidVal = {}; + const quint8 *const source = static_cast<const quint8 *>([uuid bytes]); + std::copy(source, source + 16, uuidVal.data); + return QBluetoothUuid(uuidVal); +} + +QString qt_error_string(IOReturn errorCode) +{ + switch (errorCode) { + case kIOReturnSuccess: + // NoError in many classes == an empty string description. + return QString(); + case kIOReturnNoMemory: + return QString::fromLatin1("memory allocation failed"); + case kIOReturnNoResources: + return QString::fromLatin1("failed to obtain a resource"); + case kIOReturnBusy: + return QString::fromLatin1("device is busy"); + case kIOReturnStillOpen: + return QString::fromLatin1("device(s) still open"); + // Others later ... + case kIOReturnError: // "general error" (IOReturn.h) + default: + return QString::fromLatin1("unknown error"); + } +} + +void qt_test_iobluetooth_runloop() +{ + // IOBluetooth heavily relies on a CFRunLoop machinery in a way it dispatches + // its callbacks. Technically, having a QThread with CFRunLoop-based event + // dispatcher would suffice. At the moment of writing we do not have such + // event dispatcher, so we only can work on the main thread. + if (CFRunLoopGetMain() != CFRunLoopGetCurrent()) { + qCWarning(QT_BT_DARWIN) << "IOBluetooth works only on the main thread or a" + << "thread with a running CFRunLoop"; + } +} + +#endif // !QT_IOS_BLUETOOTH + + +// Apple has: CBUUID, NSUUID, CFUUID, IOBluetoothSDPUUID +// and it's handy to have several converters: + +QBluetoothUuid qt_uuid(CBUUID *uuid) +{ + // Apples' docs say "128 bit" and "16-bit UUIDs are implicitly + // pre-filled with the Bluetooth Base UUID." + // But Core Bluetooth can return CBUUID objects of length 2 + // (16-bit, so they are not pre-filled?). + + if (!uuid) + return QBluetoothUuid(); + + QT_BT_MAC_AUTORELEASEPOOL; + + if (uuid.data.length == 2) { + // CBUUID's docs say nothing about byte-order. + // Seems to be in big-endian. + const uchar *const src = static_cast<const uchar *>(uuid.data.bytes); + return QBluetoothUuid(qFromBigEndian<quint16>(src)); + } else if (uuid.data.length == 16) { + quint128 qtUuidData = {}; + const quint8 *const source = static_cast<const quint8 *>(uuid.data.bytes); + std::copy(source, source + 16, qtUuidData.data); + + return QBluetoothUuid(qtUuidData); + } + + qCDebug(QT_BT_DARWIN) << "qt_uuid, invalid CBUUID, 2 or 16 bytes expected, but got " + << uuid.data.length << " bytes length"; + return QBluetoothUuid(); +} + +CFStrongReference<CFUUIDRef> cf_uuid(const QBluetoothUuid &qtUuid) +{ + const quint128 qtUuidData = qtUuid.toUInt128(); + const quint8 *const data = qtUuidData.data; + + CFUUIDBytes bytes = {data[0], data[1], data[2], data[3], + data[4], data[5], data[6], data[7], + data[8], data[9], data[10], data[11], + data[12], data[13], data[14], data[15]}; + + CFUUIDRef cfUuid = CFUUIDCreateFromUUIDBytes(kCFAllocatorDefault, bytes); + return CFStrongReference<CFUUIDRef>(cfUuid, false);// false == already retained. +} + +ObjCStrongReference<CBUUID> cb_uuid(const QBluetoothUuid &qtUuid) +{ + CFStrongReference<CFUUIDRef> cfUuid(cf_uuid(qtUuid)); + if (!cfUuid) + return ObjCStrongReference<CBUUID>(); + + ObjCStrongReference<CBUUID> cbUuid([CBUUID UUIDWithCFUUID:cfUuid], true); //true == retain. + return cbUuid; +} + +bool equal_uuids(const QBluetoothUuid &qtUuid, CBUUID *cbUuid) +{ + const QBluetoothUuid qtUuid2(qt_uuid(cbUuid)); + return qtUuid == qtUuid2; +} + +bool equal_uuids(CBUUID *cbUuid, const QBluetoothUuid &qtUuid) +{ + return equal_uuids(qtUuid, cbUuid); +} + +QByteArray qt_bytearray(NSData *data) +{ + QByteArray value; + if (!data || !data.length) + return value; + + value.resize(data.length); + const char *const src = static_cast<const char *>(data.bytes); + std::copy(src, src + data.length, value.data()); + + return value; +} + +template<class Integer> +QByteArray qt_bytearray(Integer n) +{ + QByteArray value; + value.resize(sizeof n); + const char *const src = reinterpret_cast<char *>(&n); + std::copy(src, src + sizeof n, value.data()); + + return value; +} + +QByteArray qt_bytearray(NSString *string) +{ + if (!string) + return QByteArray(); + + QT_BT_MAC_AUTORELEASEPOOL; + NSData *const utf8Data = [string dataUsingEncoding:NSUTF8StringEncoding]; + + return qt_bytearray(utf8Data); +} + +QByteArray qt_bytearray(NSObject *obj) +{ + // descriptor.value has type 'id'. + // While the Apple's docs say this about descriptors: + // + // - CBUUIDCharacteristicExtendedPropertiesString + // The string representation of the UUID for the extended properties descriptor. + // The corresponding value for this descriptor is an NSNumber object. + // + // - CBUUIDCharacteristicUserDescriptionString + // The string representation of the UUID for the user description descriptor. + // The corresponding value for this descriptor is an NSString object. + // + // ... etc. + // + // This is not true. On OS X, they all seem to be NSData (or derived from NSData), + // and they can be something else on iOS (NSNumber, NSString, etc.) + if (!obj) + return QByteArray(); + + QT_BT_MAC_AUTORELEASEPOOL; + + if ([obj isKindOfClass:[NSData class]]) { + return qt_bytearray(static_cast<NSData *>(obj)); + } else if ([obj isKindOfClass:[NSString class]]) { + return qt_bytearray(static_cast<NSString *>(obj)); + } else if ([obj isKindOfClass:[NSNumber class]]) { + NSNumber *const nsNumber = static_cast<NSNumber *>(obj); + return qt_bytearray([nsNumber unsignedShortValue]); + } + // TODO: Where can be more types, but Core Bluetooth does not support them, + // or at least it's not documented. + + return QByteArray(); +} + +ObjCStrongReference<NSData> data_from_bytearray(const QByteArray & qtData) +{ + if (!qtData.size()) + return ObjCStrongReference<NSData>([[NSData alloc] init], false); + + ObjCStrongReference<NSData> result([NSData dataWithBytes:qtData.constData() length:qtData.size()], true); + return result; +} + +ObjCStrongReference<NSMutableData> mutable_data_from_bytearray(const QByteArray &qtData) +{ + using MutableData = ObjCStrongReference<NSMutableData>; + + if (!qtData.size()) + return MutableData([[NSMutableData alloc] init], false); + + MutableData result([[NSMutableData alloc] initWithLength:qtData.size()], false); + [result replaceBytesInRange:NSMakeRange(0, qtData.size()) + withBytes:qtData.constData()]; + return result; +} + +// A small RAII class for a dispatch queue. +class SerialDispatchQueue +{ +public: + explicit SerialDispatchQueue(const char *label) + { + Q_ASSERT(label); + + queue = dispatch_queue_create(label, DISPATCH_QUEUE_SERIAL); + if (!queue) { + qCCritical(QT_BT_DARWIN) << "failed to create dispatch queue with label" + << label; + } + } + ~SerialDispatchQueue() + { + if (queue) + dispatch_release(queue); + } + + dispatch_queue_t data() const + { + return queue; + } +private: + dispatch_queue_t queue; + + Q_DISABLE_COPY(SerialDispatchQueue) +}; + +dispatch_queue_t qt_LE_queue() +{ + static const SerialDispatchQueue leQueue("qt-bluetooth-LE-queue"); + return leQueue.data(); +} + +} // namespace DarwinBluetooth + +QT_END_NAMESPACE diff --git a/src/bluetooth/darwin/btutility_p.h b/src/bluetooth/darwin/btutility_p.h new file mode 100644 index 00000000..3108b515 --- /dev/null +++ b/src/bluetooth/darwin/btutility_p.h @@ -0,0 +1,339 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#ifndef BTUTILITY_P_H +#define BTUTILITY_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/qloggingcategory.h> +#include <QtCore/qscopedpointer.h> +#include <QtCore/qglobal.h> + +#include <Foundation/Foundation.h> + +#include <CoreBluetooth/CoreBluetooth.h> + +#ifdef Q_OS_MACOS +#include <IOBluetooth/IOBluetooth.h> +#endif // Q_OS_MACOS + +QT_BEGIN_NAMESPACE + +class QLowEnergyCharacteristicData; +class QBluetoothAddress; +class QBluetoothUuid; + +namespace DarwinBluetooth { + +struct NSObjectDeleter { + static void cleanup(NSObject *obj) + { + [obj release]; + } +}; + +template<class T> +class ObjCScopedPointer : public QScopedPointer<NSObject, NSObjectDeleter> +{ +public: + // TODO: remove default argument, add 'retain' parameter, + // add a default ctor??? This will make the semantics more + // transparent + will simplify the future transition to ARC + // (if it will ever happen). + explicit ObjCScopedPointer(T *ptr = nullptr) : QScopedPointer(ptr){} + operator T*() const + { + return data(); + } + + T *data()const + { + return static_cast<T *>(QScopedPointer::data()); + } + + T *take() + { + return static_cast<T *>(QScopedPointer::take()); + } +}; + +#define QT_BT_MAC_AUTORELEASEPOOL const QMacAutoReleasePool pool; + +template<class T> +class ObjCStrongReference { +public: + ObjCStrongReference() + : m_ptr(nil) + { + } + ObjCStrongReference(T *obj, bool retain) + { + if (retain) + m_ptr = [obj retain]; + else + m_ptr = obj; // For example, created with initWithXXXX. + } + ObjCStrongReference(const ObjCStrongReference &rhs) + { + m_ptr = [rhs.m_ptr retain]; + } + ObjCStrongReference &operator = (const ObjCStrongReference &rhs) + { + // "Old-style" implementation: + if (this != &rhs && m_ptr != rhs.m_ptr) { + [m_ptr release]; + m_ptr = [rhs.m_ptr retain]; + } + + return *this; + } + +#ifdef Q_COMPILER_RVALUE_REFS + ObjCStrongReference(ObjCStrongReference &&xval) + { + m_ptr = xval.m_ptr; + xval.m_ptr = nil; + } + + ObjCStrongReference &operator = (ObjCStrongReference &&xval) + { + m_ptr = xval.m_ptr; + xval.m_ptr = nil; + return *this; + } +#endif + + ~ObjCStrongReference() + { + [m_ptr release]; + } + + void reset(T *newVal) + { + if (m_ptr != newVal) { + [m_ptr release]; + m_ptr = [newVal retain]; + } + } + + void resetWithoutRetain(T *newVal) + { + if (m_ptr != newVal) { + [m_ptr release]; + m_ptr = newVal; + } + } + + operator T *() const + { + return m_ptr; + } + + T *data() const + { + return m_ptr; + } + + T *take() + { + T * p = m_ptr; + m_ptr = nil; + return p; + } +private: + T *m_ptr; +}; + +// The type 'T' is some XXXRef from CoreFoundation and co. +// In principle, we can do a trick removing a pointer from a type +// when template is instantiated, but it's quite a lot of ugly pp-tokens +// like DarwinBluetooth::CFStrongReference<DarwinBluetooth::remove_pointer<CFUUIDRref>> strongReference; +// so instead we use 'T' everywhere, not 'T *' as can expected +// from a smart pointer. +template<class T> +class CFStrongReference { +public: + CFStrongReference() + : m_ptr(nullptr) + { + } + + CFStrongReference(T obj, bool retain) + : m_ptr(obj) + { + if (m_ptr && retain) + CFRetain(m_ptr); + } + + CFStrongReference(const CFStrongReference &rhs) + { + if ((m_ptr = rhs.m_ptr)) + CFRetain(m_ptr); + } + + CFStrongReference &operator = (const CFStrongReference &rhs) + { + // "Old-style" implementation: + if (this != &rhs && m_ptr != rhs.m_ptr) { + if (m_ptr) + CFRelease(m_ptr); + if ((m_ptr = rhs.m_ptr)) + CFRetain(m_ptr); + } + + return *this; + } + +#ifdef Q_COMPILER_RVALUE_REFS + CFStrongReference(CFStrongReference &&xval) + { + m_ptr = xval.m_ptr; + xval.m_ptr = nullptr; + } + + CFStrongReference &operator = (CFStrongReference &&xval) + { + m_ptr = xval.m_ptr; + xval.m_ptr = nullptr; + return *this; + } +#endif + + ~CFStrongReference() + { + if (m_ptr) + CFRelease(m_ptr); + } + + void reset(T newVal) + { + if (m_ptr != newVal) { + if (m_ptr) + CFRelease(m_ptr); + if ((m_ptr = newVal)) + CFRetain(m_ptr); + } + } + + operator T() const + { + return m_ptr; + } + + T data() const + { + return m_ptr; + } + + T take() + { + T p = m_ptr; + m_ptr = nullptr; + return p; + } +private: + T m_ptr; +}; + +QString qt_address(NSString *address); + +#ifndef QT_IOS_BLUETOOTH + +QBluetoothAddress qt_address(const BluetoothDeviceAddress *address); +BluetoothDeviceAddress iobluetooth_address(const QBluetoothAddress &address); + +ObjCStrongReference<IOBluetoothSDPUUID> iobluetooth_uuid(const QBluetoothUuid &uuid); +QBluetoothUuid qt_uuid(IOBluetoothSDPUUID *uuid); +QString qt_error_string(IOReturn errorCode); +void qt_test_iobluetooth_runloop(); + +#endif // !QT_IOS_BLUETOOTH + +QBluetoothUuid qt_uuid(CBUUID *uuid); +CFStrongReference<CFUUIDRef> cf_uuid(const QBluetoothUuid &qtUuid); +ObjCStrongReference<CBUUID> cb_uuid(const QBluetoothUuid &qtUuid); +bool equal_uuids(const QBluetoothUuid &qtUuid, CBUUID *cbUuid); +bool equal_uuids(CBUUID *cbUuid, const QBluetoothUuid &qtUuid); +QByteArray qt_bytearray(NSData *data); +QByteArray qt_bytearray(NSObject *data); + +ObjCStrongReference<NSData> data_from_bytearray(const QByteArray &qtData); +ObjCStrongReference<NSMutableData> mutable_data_from_bytearray(const QByteArray &qtData); + +dispatch_queue_t qt_LE_queue(); + +extern const int defaultLEScanTimeoutMS; +extern const int maxValueLength; + +} // namespace DarwinBluetooth + +Q_DECLARE_LOGGING_CATEGORY(QT_BT_DARWIN) + +QT_END_NAMESPACE + +#if QT_MACOS_PLATFORM_SDK_EQUAL_OR_ABOVE(101300) && QT_MACOS_DEPLOYMENT_TARGET_BELOW(101300) + + // In the macOS 10.13 SDK, the identifier property was moved from the CBPeripheral + // and CBCentral classes to a new CBPeer base class. Because CBPeer is only available + // on macOS 10.13 and above, the same is true for -[CBPeer identifier]. However, + // since we know that the derived classes have always had this property, + // we'll explicitly mark its availability here. This will not adversely affect + // using the identifier through the CBPeer base class, which will still require macOS 10.13. + +@interface CBPeripheral (UnguardedWorkaround) +@property (readonly, nonatomic) NSUUID *identifier NS_AVAILABLE(10_7, 5_0); +@end + +@interface CBCentral (UnguardedWorkaround) +@property (readonly, nonatomic) NSUUID *identifier NS_AVAILABLE(10_7, 5_0); +@end + +#endif // QT_MACOS_PLATFORM_SDK_EQUAL_OR_ABOVE + +#endif // BTUTILITY_P_H diff --git a/src/bluetooth/darwin/darwinbt.pri b/src/bluetooth/darwin/darwinbt.pri new file mode 100644 index 00000000..b419007a --- /dev/null +++ b/src/bluetooth/darwin/darwinbt.pri @@ -0,0 +1,48 @@ +SOURCES += darwin/uistrings.cpp \ + darwin/btnotifier.cpp \ + darwin/btdelegates.cpp \ + darwin/btledeviceinquiry.mm \ + darwin/btcentralmanager.mm + +HEADERS += darwin/uistrings_p.h \ + darwin/btgcdtimer_p.h \ + darwin/btraii_p.h \ + darwin/btdelegates_p.h \ + darwin/btutility_p.h \ + darwin/btledeviceinquiry_p.h \ + darwin/btcentralmanager_p.h \ + darwin/btnotifier_p.h + +OBJECTIVE_SOURCES += darwin/btgcdtimer.mm \ + darwin/btraii.mm \ + darwin/btutility.mm + +#QMAKE_CXXFLAGS_WARN_ON += -Wno-nullability-completeness + +macos { + HEADERS += darwin/btdevicepair_p.h \ + darwin/btdeviceinquiry_p.h \ + darwin/btconnectionmonitor_p.h \ + darwin/btsdpinquiry_p.h \ + darwin/btrfcommchannel_p.h \ + darwin/btl2capchannel_p.h \ + darwin/btservicerecord_p.h \ + darwin/btsocketlistener_p.h \ + darwin/btobexsession_p.h + + OBJECTIVE_SOURCES += darwin/btdevicepair.mm \ + darwin/btdeviceinquiry.mm \ + darwin/btconnectionmonitor.mm \ + darwin/btsdpinquiry.mm \ + darwin/btrfcommchannel.mm \ + darwin/btl2capchannel.mm \ + darwin/btservicerecord.mm \ + darwin/btsocketlistener.mm \ + darwin/btobexsession.mm +} + +macos | ios { + HEADERS += darwin/btperipheralmanager_p.h + + OBJECTIVE_SOURCES += darwin/btperipheralmanager.mm +} diff --git a/src/bluetooth/darwin/uistrings.cpp b/src/bluetooth/darwin/uistrings.cpp new file mode 100644 index 00000000..7260417f --- /dev/null +++ b/src/bluetooth/darwin/uistrings.cpp @@ -0,0 +1,88 @@ +/**************************************************************************** +** +** 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 "uistrings_p.h" + +// Translatable messages should go into this .cpp file for them to +// be picked up by lupdate. + +QT_BEGIN_NAMESPACE + +const char DEV_DISCOVERY[] = "QBluetoothDeviceDiscoveryAgent"; +const char DD_POWERED_OFF[] = QT_TRANSLATE_NOOP("QBluetoothDeviceDiscoveryAgent", "Device is powered off"); +const char DD_INVALID_ADAPTER[] = QT_TRANSLATE_NOOP("QBluetoothDeviceDiscoveryAgent", "Cannot find valid Bluetooth adapter."); +const char DD_IO[] = QT_TRANSLATE_NOOP("QBluetoothDeviceDiscoveryAgent", "Input Output Error"); +const char DD_NOTSUPPORTED[] = QT_TRANSLATE_NOOP("QBluetoothDeviceDiscoveryAgent", "Bluetooth LE is not supported"); +const char DD_UNKNOWN_ERROR[] = QT_TRANSLATE_NOOP("QBluetoothDeviceDiscoveryAgent", "Unknown error"); +const char DD_NOT_STARTED[] = QT_TRANSLATE_NOOP("QBluetoothDeviceDiscoveryAgent", "Cannot start device inquiry"); +const char DD_NOT_STARTED_LE[] = QT_TRANSLATE_NOOP("QBluetoothDeviceDiscoveryAgent", "Cannot start low energy device inquiry"); +const char DD_NOT_STOPPED[] = QT_TRANSLATE_NOOP("QBluetoothDeviceDiscoveryAgent", "Discovery cannot be stopped"); + +const char SERVICE_DISCOVERY[] = "QBluetoothServiceDiscoveryAgent"; +const char SD_LOCAL_DEV_OFF[] = QT_TRANSLATE_NOOP("QBluetoothServiceDiscoveryAgent", "Local device is powered off"); +const char SD_MINIMAL_FAILED[] = QT_TRANSLATE_NOOP("QBluetoothServiceDiscoveryAgent", "Minimal service discovery failed"); +const char SD_INVALID_ADDRESS[] = QT_TRANSLATE_NOOP("QBluetoothServiceDiscoveryAgent", "Invalid Bluetooth adapter address"); + +const char SOCKET[] = "QBluetoothSocket"; +const char SOC_NETWORK_ERROR[] = QT_TRANSLATE_NOOP("QBluetoothSocket", "Network Error"); +const char SOC_NOWRITE[] = QT_TRANSLATE_NOOP("QBluetoothSocket", "Cannot write while not connected"); +const char SOC_CONNECT_IN_PROGRESS[] = QT_TRANSLATE_NOOP("QBluetoothSocket", "Trying to connect while connection is in progress"); +const char SOC_SERVICE_NOT_FOUND[] = QT_TRANSLATE_NOOP("QBluetoothSocket", "Service cannot be found"); +const char SOC_INVAL_DATASIZE[] = QT_TRANSLATE_NOOP("QBluetoothSocket", "Invalid data/data size"); +const char SOC_NOREAD[] = QT_TRANSLATE_NOOP("QBluetoothSocket", "Cannot read while not connected"); + +const char TRANSFER_REPLY[] = "QBluetoothTransferReply"; +const char TR_INVAL_TARGET[] = QT_TRANSLATE_NOOP("QBluetoothTransferReply", "Invalid target address"); +const char TR_SESSION_NO_START[] = QT_TRANSLATE_NOOP("QBluetoothTransferReply", "Push session cannot be started"); +const char TR_CONNECT_FAILED[] = QT_TRANSLATE_NOOP("QBluetoothTransferReply", "Push session cannot connect"); +const char TR_FILE_NOT_EXIST[] = QT_TRANSLATE_NOOP("QBluetoothTransferReply", "Source file does not exist"); +const char TR_NOT_READ_IODEVICE[] = QT_TRANSLATE_NOOP("QBluetoothTransferReply", "QIODevice cannot be read. Make sure it is open for reading."); +const char TR_SESSION_FAILED[] = QT_TRANSLATE_NOOP("QBluetoothTransferReply", "Push session failed"); +const char TR_INVALID_DEVICE[] = QT_TRANSLATE_NOOP("QBluetoothTransferReply", "Invalid input device (null)"); +const char TR_OP_CANCEL[] = QT_TRANSLATE_NOOP("QBluetoothTransferReply", "Operation canceled"); +const char TR_IN_PROGRESS[] = QT_TRANSLATE_NOOP("QBluetoothTransferReply", "Transfer already started"); +const char TR_SERVICE_NO_FOUND[] = QT_TRANSLATE_NOOP("QBluetoothTransferReply", "Push service not found"); + +const char LE_CONTROLLER[] = "QLowEnergyController"; +const char LEC_RDEV_NO_FOUND[] = QT_TRANSLATE_NOOP("QLowEnergyController", "Remote device cannot be found"); +const char LEC_NO_LOCAL_DEV[] = QT_TRANSLATE_NOOP("QLowEnergyController", "Cannot find local adapter"); +const char LEC_IO_ERROR[] = QT_TRANSLATE_NOOP("QLowEnergyController", "Error occurred during connection I/O"); +const char LEC_UNKNOWN_ERROR[] = QT_TRANSLATE_NOOP("QLowEnergyController", "Unknown Error"); + +QT_END_NAMESPACE diff --git a/src/bluetooth/darwin/uistrings_p.h b/src/bluetooth/darwin/uistrings_p.h new file mode 100644 index 00000000..999a8c6d --- /dev/null +++ b/src/bluetooth/darwin/uistrings_p.h @@ -0,0 +1,108 @@ +/**************************************************************************** +** +** 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$ +** +****************************************************************************/ + +#ifndef TRANSLATIONS_H +#define TRANSLATIONS_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/QCoreApplication> +#include <QtCore/QString> + +QT_BEGIN_NAMESPACE + +// QBluetoothDeviceDiscoveryAgent related strings +extern const char DEV_DISCOVERY[]; +extern const char DD_POWERED_OFF[]; +extern const char DD_INVALID_ADAPTER[]; +extern const char DD_IO[]; +extern const char DD_NOTSUPPORTED[]; +extern const char DD_UNKNOWN_ERROR[]; +extern const char DD_NOT_STARTED[]; +extern const char DD_NOT_STARTED_LE[]; +extern const char DD_NOT_STOPPED[]; + +// QBluetoothServiceDiscoveryAgent related strings +extern const char SERVICE_DISCOVERY[]; +extern const char SD_LOCAL_DEV_OFF[]; +extern const char SD_MINIMAL_FAILED[]; +extern const char SD_INVALID_ADDRESS[]; + +// QBluetoothSocket related strings +extern const char SOCKET[]; +extern const char SOC_NETWORK_ERROR[]; +extern const char SOC_NOWRITE[]; +extern const char SOC_CONNECT_IN_PROGRESS[]; +extern const char SOC_SERVICE_NOT_FOUND[]; +extern const char SOC_INVAL_DATASIZE[]; +extern const char SOC_NOREAD[]; + +// QBluetoothTransferReply related strings +extern const char TRANSFER_REPLY[]; +extern const char TR_INVAL_TARGET[]; +extern const char TR_SESSION_NO_START[]; +extern const char TR_CONNECT_FAILED[]; +extern const char TR_FILE_NOT_EXIST[]; +extern const char TR_NOT_READ_IODEVICE[]; +extern const char TR_SESSION_FAILED[]; +extern const char TR_INVALID_DEVICE[]; +extern const char TR_OP_CANCEL[]; +extern const char TR_IN_PROGRESS[]; +extern const char TR_SERVICE_NO_FOUND[]; + +// QLowEnergyController related strings +extern const char LE_CONTROLLER[]; +extern const char LEC_RDEV_NO_FOUND[]; +extern const char LEC_NO_LOCAL_DEV[]; +extern const char LEC_IO_ERROR[]; +extern const char LEC_UNKNOWN_ERROR[]; + +QT_END_NAMESPACE + +#endif // TRANSLATIONS_H + |