| /**************************************************************************** |
| ** |
| ** Copyright (C) 2016 The Qt Company Ltd. |
| ** Copyright (C) 2016 Javier S. Pedro <maemo@javispedro.com> |
| ** 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 "osx/osxbtutility_p.h" |
| #include "osx/uistrings_p.h" |
| |
| #ifndef Q_OS_TVOS |
| #include "osx/osxbtperipheralmanager_p.h" |
| #endif // Q_OS_TVOS |
| |
| #include "qlowenergycontroller_darwin_p.h" |
| #include "qlowenergyserviceprivate_p.h" |
| #include "osx/osxbtcentralmanager_p.h" |
| |
| #include "qlowenergyservicedata.h" |
| #include "qbluetoothlocaldevice.h" |
| #include "qbluetoothdeviceinfo.h" |
| #include "qlowenergycontroller.h" |
| #include "qbluetoothuuid.h" |
| |
| #include <QtCore/qloggingcategory.h> |
| #include <QtCore/qsharedpointer.h> |
| #include <QtCore/qbytearray.h> |
| #include <QtCore/qglobal.h> |
| #include <QtCore/qstring.h> |
| #include <QtCore/qlist.h> |
| |
| QT_BEGIN_NAMESPACE |
| |
| namespace { |
| |
| typedef QSharedPointer<QLowEnergyServicePrivate> ServicePrivate; |
| |
| // Convenience function, can return a smart pointer that 'isNull'. |
| ServicePrivate qt_createLEService(QLowEnergyControllerPrivateDarwin *controller, CBService *cbService, bool included) |
| { |
| Q_ASSERT_X(controller, Q_FUNC_INFO, "invalid controller (null)"); |
| Q_ASSERT_X(cbService, Q_FUNC_INFO, "invalid service (nil)"); |
| |
| CBUUID *const cbUuid = cbService.UUID; |
| if (!cbUuid) { |
| qCDebug(QT_BT_OSX) << "invalid service, UUID is nil"; |
| return ServicePrivate(); |
| } |
| |
| const QBluetoothUuid qtUuid(OSXBluetooth::qt_uuid(cbUuid)); |
| if (qtUuid.isNull()) // Conversion error is reported by qt_uuid. |
| return ServicePrivate(); |
| |
| ServicePrivate newService(new QLowEnergyServicePrivate); |
| newService->uuid = qtUuid; |
| newService->setController(controller); |
| |
| if (included) |
| newService->type |= QLowEnergyService::IncludedService; |
| |
| // TODO: isPrimary is ... always 'NO' - to be investigated. |
| /* |
| if (!cbService.isPrimary) { |
| // Our guess included/not was probably wrong. |
| newService->type &= ~QLowEnergyService::PrimaryService; |
| newService->type |= QLowEnergyService::IncludedService; |
| } |
| */ |
| return newService; |
| } |
| |
| typedef QList<QBluetoothUuid> UUIDList; |
| |
| UUIDList qt_servicesUuids(NSArray *services) |
| { |
| QT_BT_MAC_AUTORELEASEPOOL; |
| |
| if (!services || !services.count) |
| return UUIDList(); |
| |
| UUIDList uuids; |
| |
| for (CBService *s in services) |
| uuids.append(OSXBluetooth::qt_uuid(s.UUID)); |
| |
| return uuids; |
| } |
| |
| } // unnamed namespace |
| |
| #ifndef Q_OS_TVOS |
| using ObjCPeripheralManager = QT_MANGLE_NAMESPACE(OSXBTPeripheralManager); |
| #endif // Q_OS_TVOS |
| |
| using ObjCCentralManager = QT_MANGLE_NAMESPACE(OSXBTCentralManager); |
| |
| QLowEnergyControllerPrivateDarwin::QLowEnergyControllerPrivateDarwin() |
| { |
| void registerQLowEnergyControllerMetaType(); |
| registerQLowEnergyControllerMetaType(); |
| qRegisterMetaType<QLowEnergyHandle>("QLowEnergyHandle"); |
| qRegisterMetaType<QSharedPointer<QLowEnergyServicePrivate>>(); |
| } |
| |
| QLowEnergyControllerPrivateDarwin::~QLowEnergyControllerPrivateDarwin() |
| { |
| if (const auto leQueue = OSXBluetooth::qt_LE_queue()) { |
| if (role == QLowEnergyController::CentralRole) { |
| const auto manager = centralManager.getAs<ObjCCentralManager>(); |
| dispatch_sync(leQueue, ^{ |
| [manager detach]; |
| }); |
| } else { |
| #ifndef Q_OS_TVOS |
| const auto manager = peripheralManager.getAs<ObjCPeripheralManager>(); |
| dispatch_sync(leQueue, ^{ |
| [manager detach]; |
| }); |
| #endif |
| } |
| } |
| } |
| |
| bool QLowEnergyControllerPrivateDarwin::isValid() const |
| { |
| #ifdef Q_OS_TVOS |
| return centralManager; |
| #else |
| return centralManager || peripheralManager; |
| #endif |
| } |
| |
| void QLowEnergyControllerPrivateDarwin::init() |
| { |
| using OSXBluetooth::LECBManagerNotifier; |
| |
| QScopedPointer<LECBManagerNotifier> notifier(new LECBManagerNotifier); |
| if (role == QLowEnergyController::PeripheralRole) { |
| #ifndef Q_OS_TVOS |
| peripheralManager.reset([[ObjCPeripheralManager alloc] initWith:notifier.data()], |
| DarwinBluetooth::RetainPolicy::noInitialRetain); |
| if (!peripheralManager) { |
| qCWarning(QT_BT_OSX) << "failed to create a peripheral manager"; |
| return; |
| } |
| #else |
| qCWarning(QT_BT_OSX) << "the peripheral role is not supported on your platform"; |
| return; |
| #endif // Q_OS_TVOS |
| } else { |
| centralManager.reset([[ObjCCentralManager alloc] initWith:notifier.data()], |
| DarwinBluetooth::RetainPolicy::noInitialRetain); |
| if (!centralManager) { |
| qCWarning(QT_BT_OSX) << "failed to initialize a central manager"; |
| return; |
| } |
| } |
| |
| if (!connectSlots(notifier.data())) |
| qCWarning(QT_BT_OSX) << "failed to connect to notifier's signal(s)"; |
| |
| // Ownership was taken by central manager. |
| notifier.take(); |
| } |
| |
| void QLowEnergyControllerPrivateDarwin::connectToDevice() |
| { |
| Q_ASSERT_X(state == QLowEnergyController::UnconnectedState, |
| Q_FUNC_INFO, "invalid state"); |
| |
| if (!isValid()) { |
| // init() had failed for was never called. |
| return _q_CBManagerError(QLowEnergyController::UnknownError); |
| } |
| |
| if (deviceUuid.isNull()) { |
| // Wrong constructor was used or invalid UUID was provided. |
| return _q_CBManagerError(QLowEnergyController::UnknownRemoteDeviceError); |
| } |
| |
| // The logic enforcing the role is in the public class. |
| Q_ASSERT_X(role != QLowEnergyController::PeripheralRole, |
| Q_FUNC_INFO, "invalid role (peripheral)"); |
| |
| dispatch_queue_t leQueue(OSXBluetooth::qt_LE_queue()); |
| if (!leQueue) { |
| qCWarning(QT_BT_OSX) << "no LE queue found"; |
| setErrorDescription(QLowEnergyController::UnknownError); |
| return; |
| } |
| |
| setErrorDescription(QLowEnergyController::NoError); |
| setState(QLowEnergyController::ConnectingState); |
| |
| const QBluetoothUuid deviceUuidCopy(deviceUuid); |
| ObjCCentralManager *manager = centralManager.getAs<ObjCCentralManager>(); |
| dispatch_async(leQueue, ^{ |
| [manager connectToDevice:deviceUuidCopy]; |
| }); |
| } |
| |
| void QLowEnergyControllerPrivateDarwin::disconnectFromDevice() |
| { |
| if (role == QLowEnergyController::PeripheralRole) { |
| // CoreBluetooth API intentionally does not provide any way of closing |
| // a connection. All we can do here is to stop the advertisement. |
| stopAdvertising(); |
| return; |
| } |
| |
| if (isValid()) { |
| const auto oldState = state; |
| |
| if (dispatch_queue_t leQueue = OSXBluetooth::qt_LE_queue()) { |
| setState(QLowEnergyController::ClosingState); |
| invalidateServices(); |
| |
| auto manager = centralManager.getAs<ObjCCentralManager>(); |
| dispatch_async(leQueue, ^{ |
| [manager disconnectFromDevice]; |
| }); |
| |
| if (oldState == QLowEnergyController::ConnectingState) { |
| // With a pending connect attempt there is no |
| // guarantee we'll ever have didDisconnect callback, |
| // set the state here and now to make sure we still |
| // can connect. |
| setState(QLowEnergyController::UnconnectedState); |
| } |
| } else { |
| qCCritical(QT_BT_OSX) << "qt LE queue is nil, " |
| "can not dispatch 'disconnect'"; |
| } |
| } |
| } |
| |
| void QLowEnergyControllerPrivateDarwin::discoverServices() |
| { |
| Q_ASSERT_X(state != QLowEnergyController::UnconnectedState, |
| Q_FUNC_INFO, "not connected to peripheral"); |
| Q_ASSERT_X(role != QLowEnergyController::PeripheralRole, |
| Q_FUNC_INFO, "invalid role (peripheral)"); |
| |
| dispatch_queue_t leQueue(OSXBluetooth::qt_LE_queue()); |
| Q_ASSERT_X(leQueue, Q_FUNC_INFO, "LE queue not found"); |
| |
| setState(QLowEnergyController::DiscoveringState); |
| |
| ObjCCentralManager *manager = centralManager.getAs<ObjCCentralManager>(); |
| dispatch_async(leQueue, ^{ |
| [manager discoverServices]; |
| }); |
| } |
| |
| void QLowEnergyControllerPrivateDarwin::discoverServiceDetails(const QBluetoothUuid &serviceUuid) |
| { |
| if (state != QLowEnergyController::DiscoveredState) { |
| qCWarning(QT_BT_OSX) << "can not discover service details in the current state, " |
| "QLowEnergyController::DiscoveredState is expected"; |
| return; |
| } |
| |
| if (!serviceList.contains(serviceUuid)) { |
| qCWarning(QT_BT_OSX) << "unknown service: " << serviceUuid; |
| return; |
| } |
| |
| dispatch_queue_t leQueue(OSXBluetooth::qt_LE_queue()); |
| Q_ASSERT(leQueue); |
| |
| ServicePrivate qtService(serviceList.value(serviceUuid)); |
| qtService->setState(QLowEnergyService::DiscoveringServices); |
| // Copy objects ... |
| ObjCCentralManager *manager = centralManager.getAs<ObjCCentralManager>(); |
| const QBluetoothUuid serviceUuidCopy(serviceUuid); |
| dispatch_async(leQueue, ^{ |
| [manager discoverServiceDetails:serviceUuidCopy]; |
| }); |
| } |
| |
| void QLowEnergyControllerPrivateDarwin::requestConnectionUpdate(const QLowEnergyConnectionParameters ¶ms) |
| { |
| Q_UNUSED(params); |
| // TODO: implement this, if possible. |
| qCWarning(QT_BT_OSX) << "Connection update not implemented on your platform"; |
| } |
| |
| void QLowEnergyControllerPrivateDarwin::addToGenericAttributeList(const QLowEnergyServiceData &service, |
| QLowEnergyHandle startHandle) |
| { |
| Q_UNUSED(service); |
| Q_UNUSED(startHandle); |
| // TODO: check why I don't need this (apparently it is used in addServiceHelper |
| // of the base class). |
| } |
| |
| QLowEnergyService * QLowEnergyControllerPrivateDarwin::addServiceHelper(const QLowEnergyServiceData &service) |
| { |
| // Three checks below should be removed, they are done in the q_ptr's class. |
| #ifdef Q_OS_TVOS |
| Q_UNUSED(service); |
| qCDebug(QT_BT_OSX, "peripheral role is not supported on tvOS"); |
| #else |
| if (role != QLowEnergyController::PeripheralRole) { |
| qCWarning(QT_BT_OSX) << "not in peripheral role"; |
| return nullptr; |
| } |
| |
| if (state != QLowEnergyController::UnconnectedState) { |
| qCWarning(QT_BT_OSX) << "invalid state"; |
| return nullptr; |
| } |
| |
| if (!service.isValid()) { |
| qCWarning(QT_BT_OSX) << "invalid service"; |
| return nullptr; |
| } |
| |
| for (auto includedService : service.includedServices()) |
| includedService->d_ptr->type |= QLowEnergyService::IncludedService; |
| |
| const auto manager = peripheralManager.getAs<ObjCPeripheralManager>(); |
| Q_ASSERT(manager); |
| if (const auto servicePrivate = [manager addService:service]) { |
| servicePrivate->setController(this); |
| servicePrivate->state = QLowEnergyService::LocalService; |
| localServices.insert(servicePrivate->uuid, servicePrivate); |
| return new QLowEnergyService(servicePrivate); |
| } |
| #endif // Q_OS_TVOS |
| return nullptr; |
| } |
| |
| void QLowEnergyControllerPrivateDarwin::_q_connected() |
| { |
| setState(QLowEnergyController::ConnectedState); |
| emit q_ptr->connected(); |
| } |
| |
| void QLowEnergyControllerPrivateDarwin::_q_disconnected() |
| { |
| if (role == QLowEnergyController::CentralRole) |
| invalidateServices(); |
| |
| setState(QLowEnergyController::UnconnectedState); |
| emit q_ptr->disconnected(); |
| } |
| |
| void QLowEnergyControllerPrivateDarwin::_q_serviceDiscoveryFinished() |
| { |
| Q_ASSERT_X(state == QLowEnergyController::DiscoveringState, |
| Q_FUNC_INFO, "invalid state"); |
| |
| using namespace OSXBluetooth; |
| |
| QT_BT_MAC_AUTORELEASEPOOL; |
| |
| NSArray *const services = [centralManager.getAs<ObjCCentralManager>() peripheral].services; |
| // Now we have to traverse the discovered services tree. |
| // Essentially it's an iterative version of more complicated code from the |
| // OSXBTCentralManager's code. |
| // All Obj-C entities either auto-release, or guarded by ObjCScopedReferences. |
| if (services && [services count]) { |
| QMap<QBluetoothUuid, CBService *> discoveredCBServices; |
| //1. The first pass - none of this services is 'included' yet (we'll discover 'included' |
| // during the pass 2); we also ignore duplicates (== services with the same UUID) |
| // - since we do not have a way to distinguish them later |
| // (our API is using uuids when creating QLowEnergyServices). |
| for (CBService *cbService in services) { |
| const ServicePrivate newService(qt_createLEService(this, cbService, false)); |
| if (!newService.data()) |
| continue; |
| if (serviceList.contains(newService->uuid)) { |
| // It's a bit stupid we first created it ... |
| qCDebug(QT_BT_OSX) << "discovered service with a duplicated UUID" |
| << newService->uuid; |
| continue; |
| } |
| serviceList.insert(newService->uuid, newService); |
| discoveredCBServices.insert(newService->uuid, cbService); |
| } |
| |
| ObjCStrongReference<NSMutableArray> toVisit([[NSMutableArray alloc] initWithArray:services], false); |
| ObjCStrongReference<NSMutableArray> toVisitNext([[NSMutableArray alloc] init], false); |
| ObjCStrongReference<NSMutableSet> visited([[NSMutableSet alloc] init], false); |
| |
| while (true) { |
| for (NSUInteger i = 0, e = [toVisit count]; i < e; ++i) { |
| CBService *const s = [toVisit objectAtIndex:i]; |
| if (![visited containsObject:s]) { |
| [visited addObject:s]; |
| if (s.includedServices && s.includedServices.count) |
| [toVisitNext addObjectsFromArray:s.includedServices]; |
| } |
| |
| const QBluetoothUuid uuid(qt_uuid(s.UUID)); |
| if (serviceList.contains(uuid) && discoveredCBServices.value(uuid) == s) { |
| ServicePrivate qtService(serviceList.value(uuid)); |
| // Add included UUIDs: |
| qtService->includedServices.append(qt_servicesUuids(s.includedServices)); |
| }// Else - we ignored this CBService object. |
| } |
| |
| if (![toVisitNext count]) |
| break; |
| |
| for (NSUInteger i = 0, e = [toVisitNext count]; i < e; ++i) { |
| CBService *const s = [toVisitNext objectAtIndex:i]; |
| const QBluetoothUuid uuid(qt_uuid(s.UUID)); |
| if (serviceList.contains(uuid)) { |
| if (discoveredCBServices.value(uuid) == s) { |
| ServicePrivate qtService(serviceList.value(uuid)); |
| qtService->type |= QLowEnergyService::IncludedService; |
| } // Else this is the duplicate we ignored already. |
| } else { |
| // Oh, we do not even have it yet??? |
| ServicePrivate newService(qt_createLEService(this, s, true)); |
| serviceList.insert(newService->uuid, newService); |
| discoveredCBServices.insert(newService->uuid, s); |
| } |
| } |
| |
| toVisit.resetWithoutRetain(toVisitNext.take()); |
| toVisitNext.resetWithoutRetain([[NSMutableArray alloc] init]); |
| } |
| } else { |
| qCDebug(QT_BT_OSX) << "no services found"; |
| } |
| |
| for (ServiceMap::const_iterator it = serviceList.constBegin(); it != serviceList.constEnd(); ++it) |
| emit q_ptr->serviceDiscovered(it.key()); |
| |
| setState(QLowEnergyController::DiscoveredState); |
| emit q_ptr->discoveryFinished(); |
| } |
| |
| void QLowEnergyControllerPrivateDarwin::_q_serviceDetailsDiscoveryFinished(QSharedPointer<QLowEnergyServicePrivate> service) |
| { |
| QT_BT_MAC_AUTORELEASEPOOL; |
| |
| Q_ASSERT(service); |
| |
| if (!serviceList.contains(service->uuid)) { |
| qCDebug(QT_BT_OSX) << "unknown service uuid:" |
| << service->uuid; |
| return; |
| } |
| |
| ServicePrivate qtService(serviceList.value(service->uuid)); |
| // Assert on handles? |
| qtService->startHandle = service->startHandle; |
| qtService->endHandle = service->endHandle; |
| qtService->characteristicList = service->characteristicList; |
| |
| qtService->setState(QLowEnergyService::ServiceDiscovered); |
| } |
| |
| void QLowEnergyControllerPrivateDarwin::_q_servicesWereModified() |
| { |
| if (!(state == QLowEnergyController::DiscoveringState |
| || state == QLowEnergyController::DiscoveredState)) { |
| qCWarning(QT_BT_OSX) << "services were modified while controller is not in Discovered/Discovering state"; |
| return; |
| } |
| |
| if (state == QLowEnergyController::DiscoveredState) |
| invalidateServices(); |
| |
| setState(QLowEnergyController::ConnectedState); |
| q_ptr->discoverServices(); |
| } |
| |
| void QLowEnergyControllerPrivateDarwin::_q_characteristicRead(QLowEnergyHandle charHandle, |
| const QByteArray &value) |
| { |
| Q_ASSERT_X(charHandle, Q_FUNC_INFO, "invalid characteristic handle(0)"); |
| |
| ServicePrivate service(serviceForHandle(charHandle)); |
| if (service.isNull()) |
| return; |
| |
| QLowEnergyCharacteristic characteristic(characteristicForHandle(charHandle)); |
| if (!characteristic.isValid()) { |
| qCWarning(QT_BT_OSX) << "unknown characteristic"; |
| return; |
| } |
| |
| if (characteristic.properties() & QLowEnergyCharacteristic::Read) |
| updateValueOfCharacteristic(charHandle, value, false); |
| |
| emit service->characteristicRead(characteristic, value); |
| } |
| |
| void QLowEnergyControllerPrivateDarwin::_q_characteristicWritten(QLowEnergyHandle charHandle, |
| const QByteArray &value) |
| { |
| Q_ASSERT_X(charHandle, Q_FUNC_INFO, "invalid characteristic handle(0)"); |
| |
| ServicePrivate service(serviceForHandle(charHandle)); |
| if (service.isNull()) { |
| qCWarning(QT_BT_OSX) << "can not find service for characteristic handle" |
| << charHandle; |
| return; |
| } |
| |
| QLowEnergyCharacteristic characteristic(characteristicForHandle(charHandle)); |
| if (!characteristic.isValid()) { |
| qCWarning(QT_BT_OSX) << "unknown characteristic"; |
| return; |
| } |
| |
| if (characteristic.properties() & QLowEnergyCharacteristic::Read) |
| updateValueOfCharacteristic(charHandle, value, false); |
| |
| emit service->characteristicWritten(characteristic, value); |
| } |
| |
| void QLowEnergyControllerPrivateDarwin::_q_characteristicUpdated(QLowEnergyHandle charHandle, |
| const QByteArray &value) |
| { |
| // TODO: write/update notifications are quite similar (except asserts/warnings messages |
| // and different signals emitted). Merge them into one function? |
| Q_ASSERT_X(charHandle, Q_FUNC_INFO, "invalid characteristic handle(0)"); |
| |
| ServicePrivate service(serviceForHandle(charHandle)); |
| if (service.isNull()) { |
| // This can be an error (no characteristic found for this handle), |
| // it can also be that we set notify value before the service |
| // was reported (serviceDetailsDiscoveryFinished) - this happens, |
| // if we read a descriptor (characteristic client configuration), |
| // and it's (pre)set. |
| return; |
| } |
| |
| QLowEnergyCharacteristic characteristic(characteristicForHandle(charHandle)); |
| if (!characteristic.isValid()) { |
| qCWarning(QT_BT_OSX) << "unknown characteristic"; |
| return; |
| } |
| |
| if (characteristic.properties() & QLowEnergyCharacteristic::Read) |
| updateValueOfCharacteristic(charHandle, value, false); |
| |
| emit service->characteristicChanged(characteristic, value); |
| } |
| |
| void QLowEnergyControllerPrivateDarwin::_q_descriptorRead(QLowEnergyHandle dHandle, |
| const QByteArray &value) |
| { |
| Q_ASSERT_X(dHandle, Q_FUNC_INFO, "invalid descriptor handle (0)"); |
| |
| const QLowEnergyDescriptor qtDescriptor(descriptorForHandle(dHandle)); |
| if (!qtDescriptor.isValid()) { |
| qCWarning(QT_BT_OSX) << "unknown descriptor" << dHandle; |
| return; |
| } |
| |
| ServicePrivate service(serviceForHandle(qtDescriptor.characteristicHandle())); |
| updateValueOfDescriptor(qtDescriptor.characteristicHandle(), dHandle, value, false); |
| emit service->descriptorRead(qtDescriptor, value); |
| } |
| |
| void QLowEnergyControllerPrivateDarwin::_q_descriptorWritten(QLowEnergyHandle dHandle, |
| const QByteArray &value) |
| { |
| Q_ASSERT_X(dHandle, Q_FUNC_INFO, "invalid descriptor handle (0)"); |
| |
| const QLowEnergyDescriptor qtDescriptor(descriptorForHandle(dHandle)); |
| if (!qtDescriptor.isValid()) { |
| qCWarning(QT_BT_OSX) << "unknown descriptor" << dHandle; |
| return; |
| } |
| |
| ServicePrivate service(serviceForHandle(qtDescriptor.characteristicHandle())); |
| // TODO: test if this data is what we expected. |
| updateValueOfDescriptor(qtDescriptor.characteristicHandle(), dHandle, value, false); |
| emit service->descriptorWritten(qtDescriptor, value); |
| } |
| |
| void QLowEnergyControllerPrivateDarwin::_q_notificationEnabled(QLowEnergyHandle charHandle, |
| bool enabled) |
| { |
| // CoreBluetooth in peripheral role does not allow mutable descriptors, |
| // in central we can only call setNotification:enabled/disabled. |
| // But from Qt API's point of view, a central has to write into |
| // client characteristic configuration descriptor. So here we emulate |
| // such a write (we cannot say if it's a notification or indication and |
| // report as both). |
| |
| Q_ASSERT_X(role == QLowEnergyController::PeripheralRole, Q_FUNC_INFO, |
| "controller has an invalid role, 'peripheral' expected"); |
| Q_ASSERT_X(charHandle, Q_FUNC_INFO, "invalid characteristic handle (0)"); |
| |
| const QLowEnergyCharacteristic qtChar(characteristicForHandle(charHandle)); |
| if (!qtChar.isValid()) { |
| qCWarning(QT_BT_OSX) << "unknown characteristic" << charHandle; |
| return; |
| } |
| |
| const QLowEnergyDescriptor qtDescriptor = |
| qtChar.descriptor(QBluetoothUuid::ClientCharacteristicConfiguration); |
| if (!qtDescriptor.isValid()) { |
| qCWarning(QT_BT_OSX) << "characteristic" << charHandle |
| << "does not have a client characteristic " |
| "descriptor"; |
| return; |
| } |
| |
| ServicePrivate service(serviceForHandle(charHandle)); |
| if (service.data()) { |
| // It's a 16-bit value, the least significant bit is for notifications, |
| // the next one - for indications (thus 1 means notifications enabled, |
| // 2 - indications enabled). |
| // 3 is the maximum value and it means both enabled. |
| QByteArray value(2, 0); |
| if (enabled) |
| value[0] = 3; |
| updateValueOfDescriptor(charHandle, qtDescriptor.handle(), value, false); |
| emit service->descriptorWritten(qtDescriptor, value); |
| } |
| } |
| |
| void QLowEnergyControllerPrivateDarwin::_q_LEnotSupported() |
| { |
| // Report as an error. But this should not be possible |
| // actually: before connecting to any device, we have |
| // to discover it, if it was discovered ... LE _must_ |
| // be supported. |
| } |
| |
| void QLowEnergyControllerPrivateDarwin::_q_CBManagerError(QLowEnergyController::Error errorCode) |
| { |
| // This function handles errors reported while connecting to a remote device |
| // and also other errors in general. |
| setError(errorCode); |
| |
| if (state == QLowEnergyController::ConnectingState) |
| setState(QLowEnergyController::UnconnectedState); |
| else if (state == QLowEnergyController::DiscoveringState) |
| setState(QLowEnergyController::ConnectedState); |
| |
| // In any other case we stay in Discovered, it's |
| // a service/characteristic - related error. |
| } |
| |
| void QLowEnergyControllerPrivateDarwin::_q_CBManagerError(const QBluetoothUuid &serviceUuid, |
| QLowEnergyController::Error errorCode) |
| { |
| // Errors reported while discovering service details etc. |
| Q_UNUSED(errorCode) // TODO: setError? |
| |
| // We failed to discover any characteristics/descriptors. |
| if (serviceList.contains(serviceUuid)) { |
| ServicePrivate qtService(serviceList.value(serviceUuid)); |
| qtService->setState(QLowEnergyService::InvalidService); |
| } else { |
| qCDebug(QT_BT_OSX) << "error reported for unknown service" |
| << serviceUuid; |
| } |
| } |
| |
| void QLowEnergyControllerPrivateDarwin::_q_CBManagerError(const QBluetoothUuid &serviceUuid, |
| QLowEnergyService::ServiceError errorCode) |
| { |
| if (!serviceList.contains(serviceUuid)) { |
| qCDebug(QT_BT_OSX) << "unknown service uuid:" |
| << serviceUuid; |
| return; |
| } |
| |
| ServicePrivate service(serviceList.value(serviceUuid)); |
| service->setError(errorCode); |
| } |
| |
| void QLowEnergyControllerPrivateDarwin::setNotifyValue(QSharedPointer<QLowEnergyServicePrivate> service, |
| QLowEnergyHandle charHandle, |
| const QByteArray &newValue) |
| { |
| Q_ASSERT_X(!service.isNull(), Q_FUNC_INFO, "invalid service (null)"); |
| |
| if (role == QLowEnergyController::PeripheralRole) { |
| qCWarning(QT_BT_OSX) << "invalid role (peripheral)"; |
| service->setError(QLowEnergyService::DescriptorWriteError); |
| return; |
| } |
| |
| if (newValue.size() > 2) { |
| // Qt's API requires an error on such write. |
| // With Core Bluetooth we do not write any descriptor, |
| // but instead call a special method. So it's better to |
| // intercept wrong data size here: |
| qCWarning(QT_BT_OSX) << "client characteristic configuration descriptor" |
| "is 2 bytes, but value size is: " << newValue.size(); |
| service->setError(QLowEnergyService::DescriptorWriteError); |
| return; |
| } |
| |
| if (!serviceList.contains(service->uuid)) { |
| qCWarning(QT_BT_OSX) << "no service with uuid:" << service->uuid << "found"; |
| return; |
| } |
| |
| if (!service->characteristicList.contains(charHandle)) { |
| qCDebug(QT_BT_OSX) << "no characteristic with handle:" |
| << charHandle << "found"; |
| return; |
| } |
| |
| dispatch_queue_t leQueue(OSXBluetooth::qt_LE_queue()); |
| Q_ASSERT_X(leQueue, Q_FUNC_INFO, "no LE queue found"); |
| |
| ObjCCentralManager *manager = centralManager.getAs<ObjCCentralManager>(); |
| const QBluetoothUuid serviceUuid(service->uuid); |
| const QByteArray newValueCopy(newValue); |
| dispatch_async(leQueue, ^{ |
| [manager setNotifyValue:newValueCopy |
| forCharacteristic:charHandle |
| onService:serviceUuid]; |
| }); |
| } |
| |
| void QLowEnergyControllerPrivateDarwin::readCharacteristic(const QSharedPointer<QLowEnergyServicePrivate> service, |
| const QLowEnergyHandle charHandle) |
| { |
| Q_ASSERT_X(!service.isNull(), Q_FUNC_INFO, "invalid service (null)"); |
| |
| if (role == QLowEnergyController::PeripheralRole) { |
| qCWarning(QT_BT_OSX) << "invalid role (peripheral)"; |
| return; |
| } |
| |
| if (!serviceList.contains(service->uuid)) { |
| qCWarning(QT_BT_OSX) << "no service with uuid:" |
| << service->uuid << "found"; |
| return; |
| } |
| |
| if (!service->characteristicList.contains(charHandle)) { |
| qCDebug(QT_BT_OSX) << "no characteristic with handle:" |
| << charHandle << "found"; |
| return; |
| } |
| |
| dispatch_queue_t leQueue(OSXBluetooth::qt_LE_queue()); |
| Q_ASSERT_X(leQueue, Q_FUNC_INFO, "no LE queue found"); |
| |
| // Attention! We have to copy UUID. |
| ObjCCentralManager *manager = centralManager.getAs<ObjCCentralManager>(); |
| const QBluetoothUuid serviceUuid(service->uuid); |
| dispatch_async(leQueue, ^{ |
| [manager readCharacteristic:charHandle onService:serviceUuid]; |
| }); |
| } |
| |
| void QLowEnergyControllerPrivateDarwin::writeCharacteristic(const QSharedPointer<QLowEnergyServicePrivate> service, |
| const QLowEnergyHandle charHandle, const QByteArray &newValue, |
| QLowEnergyService::WriteMode mode) |
| { |
| Q_ASSERT_X(!service.isNull(), Q_FUNC_INFO, "invalid service (null)"); |
| |
| // We can work only with services found on a given peripheral |
| // (== created by the given LE controller). |
| |
| if (!serviceList.contains(service->uuid) && !localServices.contains(service->uuid)) { |
| qCWarning(QT_BT_OSX) << "no service with uuid:" |
| << service->uuid << " found"; |
| return; |
| } |
| |
| if (!service->characteristicList.contains(charHandle)) { |
| qCDebug(QT_BT_OSX) << "no characteristic with handle:" |
| << charHandle << " found"; |
| return; |
| } |
| |
| dispatch_queue_t leQueue(OSXBluetooth::qt_LE_queue()); |
| Q_ASSERT_X(leQueue, Q_FUNC_INFO, "no LE queue found"); |
| // Attention! We have to copy objects! |
| const QByteArray newValueCopy(newValue); |
| if (role == QLowEnergyController::CentralRole) { |
| const QBluetoothUuid serviceUuid(service->uuid); |
| const auto manager = centralManager.getAs<ObjCCentralManager>(); |
| dispatch_async(leQueue, ^{ |
| [manager write:newValueCopy |
| charHandle:charHandle |
| onService:serviceUuid |
| withResponse:mode == QLowEnergyService::WriteWithResponse]; |
| }); |
| } else { |
| #ifndef Q_OS_TVOS |
| const auto manager = peripheralManager.getAs<ObjCPeripheralManager>(); |
| dispatch_async(leQueue, ^{ |
| [manager write:newValueCopy charHandle:charHandle]; |
| }); |
| #else |
| qCWarning(QT_BT_OSX) << "peripheral role is not supported on your platform"; |
| #endif |
| } |
| } |
| |
| quint16 QLowEnergyControllerPrivateDarwin::updateValueOfCharacteristic(QLowEnergyHandle charHandle, |
| const QByteArray &value, |
| bool appendValue) |
| { |
| QSharedPointer<QLowEnergyServicePrivate> service = serviceForHandle(charHandle); |
| if (!service.isNull()) { |
| CharacteristicDataMap::iterator charIt = service->characteristicList.find(charHandle); |
| if (charIt != service->characteristicList.end()) { |
| QLowEnergyServicePrivate::CharData &charData = charIt.value(); |
| if (appendValue) |
| charData.value += value; |
| else |
| charData.value = value; |
| |
| return charData.value.size(); |
| } |
| } |
| |
| return 0; |
| } |
| |
| void QLowEnergyControllerPrivateDarwin::readDescriptor(const QSharedPointer<QLowEnergyServicePrivate> service, |
| const QLowEnergyHandle charHandle, |
| const QLowEnergyHandle descriptorHandle) |
| { |
| Q_UNUSED(charHandle) // Hehe, yes! |
| |
| Q_ASSERT_X(!service.isNull(), Q_FUNC_INFO, "invalid service (null)"); |
| |
| if (role == QLowEnergyController::PeripheralRole) { |
| qCWarning(QT_BT_OSX) << "invalid role (peripheral)"; |
| return; |
| } |
| |
| if (!serviceList.contains(service->uuid)) { |
| qCWarning(QT_BT_OSX) << "no service with uuid:" |
| << service->uuid << "found"; |
| return; |
| } |
| |
| dispatch_queue_t leQueue(OSXBluetooth::qt_LE_queue()); |
| if (!leQueue) { |
| qCWarning(QT_BT_OSX) << "no LE queue found"; |
| return; |
| } |
| // Attention! Copy objects! |
| const QBluetoothUuid serviceUuid(service->uuid); |
| ObjCCentralManager * const manager = centralManager.getAs<ObjCCentralManager>(); |
| dispatch_async(leQueue, ^{ |
| [manager readDescriptor:descriptorHandle |
| onService:serviceUuid]; |
| }); |
| } |
| |
| void QLowEnergyControllerPrivateDarwin::writeDescriptor(const QSharedPointer<QLowEnergyServicePrivate> service, |
| const QLowEnergyHandle charHandle, |
| const QLowEnergyHandle descriptorHandle, |
| const QByteArray &newValue) |
| { |
| Q_UNUSED(charHandle) |
| |
| Q_ASSERT_X(!service.isNull(), Q_FUNC_INFO, "invalid service (null)"); |
| |
| if (role == QLowEnergyController::PeripheralRole) { |
| qCWarning(QT_BT_OSX) << "invalid role (peripheral)"; |
| return; |
| } |
| |
| // We can work only with services found on a given peripheral |
| // (== created by the given LE controller), |
| // otherwise we can not write anything at all. |
| if (!serviceList.contains(service->uuid)) { |
| qCWarning(QT_BT_OSX) << "no service with uuid:" |
| << service->uuid << " found"; |
| return; |
| } |
| |
| dispatch_queue_t leQueue(OSXBluetooth::qt_LE_queue()); |
| Q_ASSERT_X(leQueue, Q_FUNC_INFO, "no LE queue found"); |
| // Attention! Copy objects! |
| const QBluetoothUuid serviceUuid(service->uuid); |
| ObjCCentralManager * const manager = centralManager.getAs<ObjCCentralManager>(); |
| const QByteArray newValueCopy(newValue); |
| dispatch_async(leQueue, ^{ |
| [manager write:newValueCopy |
| descHandle:descriptorHandle |
| onService:serviceUuid]; |
| }); |
| } |
| |
| quint16 QLowEnergyControllerPrivateDarwin::updateValueOfDescriptor(QLowEnergyHandle charHandle, QLowEnergyHandle descHandle, |
| const QByteArray &value, bool appendValue) |
| { |
| QSharedPointer<QLowEnergyServicePrivate> service = serviceForHandle(charHandle); |
| if (!service.isNull()) { |
| CharacteristicDataMap::iterator charIt = service->characteristicList.find(charHandle); |
| if (charIt != service->characteristicList.end()) { |
| QLowEnergyServicePrivate::CharData &charData = charIt.value(); |
| |
| DescriptorDataMap::iterator descIt = charData.descriptorList.find(descHandle); |
| if (descIt != charData.descriptorList.end()) { |
| QLowEnergyServicePrivate::DescData &descDetails = descIt.value(); |
| |
| if (appendValue) |
| descDetails.value += value; |
| else |
| descDetails.value = value; |
| |
| return descDetails.value.size(); |
| } |
| } |
| } |
| |
| return 0; |
| } |
| |
| void QLowEnergyControllerPrivateDarwin::setErrorDescription(QLowEnergyController::Error errorCode) |
| { |
| // This function does not emit! |
| // TODO: well, it is not a reason to duplicate a significant part of |
| // setError though! |
| |
| error = errorCode; |
| |
| switch (error) { |
| case QLowEnergyController::NoError: |
| errorString.clear(); |
| break; |
| case QLowEnergyController::UnknownRemoteDeviceError: |
| errorString = QLowEnergyController::tr("Remote device cannot be found"); |
| break; |
| case QLowEnergyController::InvalidBluetoothAdapterError: |
| errorString = QLowEnergyController::tr("Cannot find local adapter"); |
| break; |
| case QLowEnergyController::NetworkError: |
| errorString = QLowEnergyController::tr("Error occurred during connection I/O"); |
| break; |
| case QLowEnergyController::ConnectionError: |
| errorString = QLowEnergyController::tr("Error occurred trying to connect to remote device."); |
| break; |
| case QLowEnergyController::AdvertisingError: |
| errorString = QLowEnergyController::tr("Error occurred trying to start advertising"); |
| break; |
| case QLowEnergyController::UnknownError: |
| default: |
| errorString = QLowEnergyController::tr("Unknown Error"); |
| break; |
| } |
| } |
| |
| bool QLowEnergyControllerPrivateDarwin::connectSlots(OSXBluetooth::LECBManagerNotifier *notifier) |
| { |
| using OSXBluetooth::LECBManagerNotifier; |
| |
| Q_ASSERT_X(notifier, Q_FUNC_INFO, "invalid notifier object (null)"); |
| |
| bool ok = connect(notifier, &LECBManagerNotifier::connected, |
| this, &QLowEnergyControllerPrivateDarwin::_q_connected); |
| ok = ok && connect(notifier, &LECBManagerNotifier::disconnected, |
| this, &QLowEnergyControllerPrivateDarwin::_q_disconnected); |
| ok = ok && connect(notifier, &LECBManagerNotifier::serviceDiscoveryFinished, |
| this, &QLowEnergyControllerPrivateDarwin::_q_serviceDiscoveryFinished); |
| ok = ok && connect(notifier, &LECBManagerNotifier::servicesWereModified, |
| this, &QLowEnergyControllerPrivateDarwin::_q_servicesWereModified); |
| ok = ok && connect(notifier, &LECBManagerNotifier::serviceDetailsDiscoveryFinished, |
| this, &QLowEnergyControllerPrivateDarwin::_q_serviceDetailsDiscoveryFinished); |
| ok = ok && connect(notifier, &LECBManagerNotifier::characteristicRead, |
| this, &QLowEnergyControllerPrivateDarwin::_q_characteristicRead); |
| ok = ok && connect(notifier, &LECBManagerNotifier::characteristicWritten, |
| this, &QLowEnergyControllerPrivateDarwin::_q_characteristicWritten); |
| ok = ok && connect(notifier, &LECBManagerNotifier::characteristicUpdated, |
| this, &QLowEnergyControllerPrivateDarwin::_q_characteristicUpdated); |
| ok = ok && connect(notifier, &LECBManagerNotifier::descriptorRead, |
| this, &QLowEnergyControllerPrivateDarwin::_q_descriptorRead); |
| ok = ok && connect(notifier, &LECBManagerNotifier::descriptorWritten, |
| this, &QLowEnergyControllerPrivateDarwin::_q_descriptorWritten); |
| ok = ok && connect(notifier, &LECBManagerNotifier::notificationEnabled, |
| this, &QLowEnergyControllerPrivateDarwin::_q_notificationEnabled); |
| ok = ok && connect(notifier, &LECBManagerNotifier::LEnotSupported, |
| this, &QLowEnergyControllerPrivateDarwin::_q_LEnotSupported); |
| ok = ok && connect(notifier, SIGNAL(CBManagerError(QLowEnergyController::Error)), |
| this, SLOT(_q_CBManagerError(QLowEnergyController::Error))); |
| ok = ok && connect(notifier, SIGNAL(CBManagerError(const QBluetoothUuid &, QLowEnergyController::Error)), |
| this, SLOT(_q_CBManagerError(const QBluetoothUuid &, QLowEnergyController::Error))); |
| ok = ok && connect(notifier, SIGNAL(CBManagerError(const QBluetoothUuid &, QLowEnergyService::ServiceError)), |
| this, SLOT(_q_CBManagerError(const QBluetoothUuid &, QLowEnergyService::ServiceError))); |
| |
| if (!ok) |
| notifier->disconnect(); |
| |
| return ok; |
| } |
| |
| void QLowEnergyControllerPrivateDarwin::startAdvertising(const QLowEnergyAdvertisingParameters ¶ms, |
| const QLowEnergyAdvertisingData &advertisingData, |
| const QLowEnergyAdvertisingData &scanResponseData) |
| { |
| #ifdef Q_OS_TVOS |
| Q_UNUSED(params) |
| Q_UNUSED(advertisingData) |
| Q_UNUSED(scanResponseData) |
| qCWarning(QT_BT_OSX) << "advertising is not supported on your platform"; |
| #else |
| |
| if (!isValid()) |
| return _q_CBManagerError(QLowEnergyController::UnknownError); |
| |
| if (role != QLowEnergyController::PeripheralRole) { |
| qCWarning(QT_BT_OSX) << "controller is not a peripheral, cannot start advertising"; |
| return; |
| } |
| |
| if (state != QLowEnergyController::UnconnectedState) { |
| qCWarning(QT_BT_OSX) << "invalid state" << state; |
| return; |
| } |
| |
| auto leQueue(OSXBluetooth::qt_LE_queue()); |
| if (!leQueue) { |
| qCWarning(QT_BT_OSX) << "no LE queue found"; |
| setErrorDescription(QLowEnergyController::UnknownError); |
| return; |
| } |
| |
| const auto manager = peripheralManager.getAs<ObjCPeripheralManager>(); |
| [manager setParameters:params data:advertisingData scanResponse:scanResponseData]; |
| |
| setState(QLowEnergyController::AdvertisingState); |
| |
| dispatch_async(leQueue, ^{ |
| [manager startAdvertising]; |
| }); |
| #endif |
| } |
| |
| void QLowEnergyControllerPrivateDarwin::stopAdvertising() |
| { |
| #ifdef Q_OS_TVOS |
| qCWarning(QT_BT_OSX) << "advertising is not supported on your platform"; |
| #else |
| if (!isValid()) |
| return _q_CBManagerError(QLowEnergyController::UnknownError); |
| |
| if (state != QLowEnergyController::AdvertisingState) { |
| qCDebug(QT_BT_OSX) << "cannot stop advertising, called in state" << state; |
| return; |
| } |
| |
| if (const auto leQueue = OSXBluetooth::qt_LE_queue()) { |
| const auto manager = peripheralManager.getAs<ObjCPeripheralManager>(); |
| dispatch_sync(leQueue, ^{ |
| [manager stopAdvertising]; |
| }); |
| |
| setState(QLowEnergyController::UnconnectedState); |
| } else { |
| qCWarning(QT_BT_OSX) << "no LE queue found"; |
| setErrorDescription(QLowEnergyController::UnknownError); |
| return; |
| } |
| #endif |
| } |
| |
| QT_END_NAMESPACE |
| |