diff options
author | Timur Pocheptsov <Timur.Pocheptsov@digia.com> | 2014-11-21 17:57:36 +0100 |
---|---|---|
committer | Timur Pocheptsov <Timur.Pocheptsov@digia.com> | 2014-11-25 15:29:16 +0100 |
commit | 4c25743910622a4834d8ebb7f3acc59e025a3c41 (patch) | |
tree | 4b2a249207ff980bace12c0d9ea75ac451241f87 /src | |
parent | 216e28ef1209ee5b28f4ca4f88cfa68a46f60c5a (diff) |
QLowEnergyController - service details discovery (OS X and iOS)
Implement details discovery: characteristics, their values (if any),
descriptors. We have to emulate handles (QLowEnergyHandle) - while
Core Bluetooth internally has the notion of handles, it never exposes
them as a public API.
Change-Id: I09158433ce6835dd34fe8ad47d047212dab59e8e
Reviewed-by: Alex Blasche <alexander.blasche@theqtcompany.com>
Diffstat (limited to 'src')
-rw-r--r-- | src/bluetooth/osx/osxbtcentralmanager.mm | 449 | ||||
-rw-r--r-- | src/bluetooth/osx/osxbtcentralmanager_p.h | 6 | ||||
-rw-r--r-- | src/bluetooth/osx/osxbtutility.mm | 35 | ||||
-rw-r--r-- | src/bluetooth/osx/osxbtutility_p.h | 3 | ||||
-rw-r--r-- | src/bluetooth/qlowenergycontroller_osx.mm | 102 | ||||
-rw-r--r-- | src/bluetooth/qlowenergycontroller_osx_p.h | 8 |
6 files changed, 588 insertions, 15 deletions
diff --git a/src/bluetooth/osx/osxbtcentralmanager.mm b/src/bluetooth/osx/osxbtcentralmanager.mm index dd36a91f..71622b1c 100644 --- a/src/bluetooth/osx/osxbtcentralmanager.mm +++ b/src/bluetooth/osx/osxbtcentralmanager.mm @@ -68,9 +68,18 @@ using namespace QT_NAMESPACE; - (QLowEnergyController::Error)connectToDevice; // "Device" is in Qt's world ... - (void)connectToPeripheral; // "Peripheral" is in Core Bluetooth. - (void)discoverIncludedServices; +- (void)readCharacteristics:(CBService *)service; +- (void)serviceDetailsDiscoveryFinished:(CBService *)service; // Aux. functions. -- (bool)connected; +- (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; @end @@ -233,6 +242,8 @@ using namespace QT_NAMESPACE; - (void)disconnectFromDevice { + servicesToDiscoverDetails.clear(); + if (managerState == OSXBluetooth::CentralManagerUpdating) { disconnectPending = true; } else { @@ -300,6 +311,291 @@ using namespace QT_NAMESPACE; } } +- (bool)discoverServiceDetails:(const QBluetoothUuid &)serviceUuid +{ + // This function does not change 'managerState', since it + // can be called concurrently (not waiting for the previous + // discovery to finish). + + using namespace OSXBluetooth; + + Q_ASSERT_X(managerState != CentralManagerUpdating, "-discoverServiceDetails:", + "invalid state"); + Q_ASSERT_X(!serviceUuid.isNull(), "-discoverServiceDetails:", + "invalid service UUID"); + Q_ASSERT_X(peripheral, "-discoverServiceDetailsl:", + "invalid peripheral (nil)"); + + if (servicesToDiscoverDetails.contains(serviceUuid)) { + qCWarning(QT_BT_OSX) << "-discoverServiceDetails: " + "already discovering for " << serviceUuid; + return true; + } + + QT_BT_MAC_AUTORELEASEPOOL; + + if (CBService *const service = [self serviceForUUID:serviceUuid]) { + servicesToDiscoverDetails.append(serviceUuid); + [peripheral discoverCharacteristics:nil forService:service]; + return true; + } + + qCWarning(QT_BT_OSX) << "-discoverServiceDetails:, invalid service - " + "unknown uuid " << serviceUuid; + + return false; +} + +- (void)readCharacteristics:(CBService *)service +{ + // This method does not change 'managerState', we can + // have several 'detail discoveries' active. + Q_ASSERT_X(service, "-readCharacteristics:", "invalid service (nil)"); + + using namespace OSXBluetooth; + + QT_BT_MAC_AUTORELEASEPOOL; + + Q_ASSERT_X(managerState != CentralManagerUpdating, "-readCharacteristics:", + "invalid state"); + Q_ASSERT_X(manager, "-readCharacteristics:", "invalid manager (nil)"); + Q_ASSERT_X(peripheral, "-readCharacteristics:", "invalid peripheral (nil)"); + Q_ASSERT_X(delegate, "-readCharacteristics:", "invalid delegate (null)"); + + if (!service.characteristics || !service.characteristics.count) + return [self serviceDetailsDiscoveryFinished:service]; + + NSArray *const cs = service.characteristics; + for (CBCharacteristic *c in cs) { + if (c.properties & CBCharacteristicPropertyRead) + 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, "-discoverDescriptors:", "invalid service (nil)"); + + using namespace OSXBluetooth; + + QT_BT_MAC_AUTORELEASEPOOL; + + Q_ASSERT_X(managerState != CentralManagerUpdating, "-discoverDescriptors", + "invalid state"); + Q_ASSERT_X(manager, "-discoverDescriptors:", "invalid manager (nil)"); + Q_ASSERT_X(peripheral, "-discoverDescriptors:", "invalid peripheral (nil)"); + + if (!service.characteristics || !service.characteristics.count) { + [self serviceDetailsDiscoveryFinished:service]; + } else { + // Start from 0 and continue in the callback. + [peripheral discoverDescriptorsForCharacteristic:[service.characteristics objectAtIndex:0]]; + } +} + +- (void)readDescriptors:(CBService *)service +{ + Q_ASSERT_X(service, "-readDescriptors:", + "invalid service (nil)"); + Q_ASSERT_X(managerState != OSXBluetooth::CentralManagerUpdating, + "-readDescriptors:", + "invalid state"); + Q_ASSERT_X(peripheral, "-readDescriptors:", + "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, "-readDescriptors:", + "invalid service"); + for (CBCharacteristic *c in cs) { + if (c.descriptors && c.descriptors.count) + return [peripheral readValueForDescriptor:[c.descriptors objectAtIndex:0]]; + } + + // No descriptors to read, done. + [self serviceDetailsDiscoveryFinished:service]; +} + +- (void)serviceDetailsDiscoveryFinished:(CBService *)service +{ + // + Q_ASSERT_X(service, "-serviceDetailsDiscoveryFinished:", + "invalid service (nil)"); + Q_ASSERT_X(delegate, "-serviceDetailsDiscoveryFinished:", + "invalid delegate (null)"); + + using namespace OSXBluetooth; + + servicesToDiscoverDetails.removeAll(qt_uuid(service.UUID)); + delegate->serviceDetailsDiscoveryFinished(ObjCStrongReference<CBService>(service, true)); +} + +// Aux. methods: + +- (CBService *)serviceForUUID:(const QBluetoothUuid &)qtUuid +{ + using namespace OSXBluetooth; + + Q_ASSERT_X(!qtUuid.isNull(), "-serviceForUUID:", + "invalid uuid"); + Q_ASSERT_X(peripheral, "-serviceForUUID:", + "invalid peripherla (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, "-nextCharacteristicForService:startingFrom:", + "invalid service (nil)"); + Q_ASSERT_X(characteristic, "-nextCharacteristicForService:startingFrom:", + "invalid characteristic (nil)"); + Q_ASSERT_X(service.characteristics, "-nextCharacteristicForService:startingFrom:", + "invalid service"); + Q_ASSERT_X(service.characteristics.count, "-nextCharacteristicForService:startingFrom:", + "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, + "-nextCharacteristicForService:startingFrom:", + "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, "-nextCharacteristicForService:startingFrom:properties:", + "invalid service (nil)"); + Q_ASSERT_X(characteristic, "-nextCharacteristicForService:startingFrom:properties:", + "invalid characteristic (nil)"); + Q_ASSERT_X(service.characteristics, "-nextCharacteristicForService:startingFrom:properties:", + "invalid service"); + Q_ASSERT_X(service.characteristics.count, "-nextCharacteristicForService:startingFrom:properties:", + "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, + "-nextCharacteristicForService:startingFrom:properties:", + "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, "-nextDescriptorForCharacteristic:startingFrom:", + "invalid characteristic (nil)"); + Q_ASSERT_X(descriptor, "-nextDescriptorForCharacteristic:startingFrom:", + "invalid descriptor (nil)"); + Q_ASSERT_X(characteristic.descriptors, + "-nextDescriptorForCharacteristic:startingFrom:", + "invalid characteristic"); + Q_ASSERT_X(characteristic.descriptors.count, + "-nextDescriptorForCharacteristic:startingFrom:", + "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, + "-nextDescriptorForCharacteristic:startingFrom:", + "descriptor was not found in characteristic.descriptors"); + + return [ds objectAtIndex:1]; +} + // CBCentralManagerDelegate (the real one). - (void)centralManagerDidUpdateState:(CBCentralManager *)central @@ -555,9 +851,156 @@ using namespace QT_NAMESPACE; - (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) - Q_UNUSED(service) - Q_UNUSED(error) + + // TODO: check that this can never be called after cancelPeripheralConnection was executed. + + using namespace OSXBluetooth; + + Q_ASSERT_X(managerState != CentralManagerUpdating, + "-peripheral:didDiscoverCharacteristicsForService:", + "invalid state"); + Q_ASSERT_X(delegate, "-peripheral:didDiscoverCharacteristicsForService:", + "invalid delegate (null)"); + + if (error) { + // NSLog to show the actual NSError (can contain something interesting). + NSLog(@"-peripheral:didDiscoverCharacteristicsForService:error, failed with error: %@", + error); + // We did not discover any characteristics and can not discover descriptors, + // inform our delegate (it will set a service state also). + delegate->error(qt_uuid(service.UUID), QLowEnergyController::UnknownError); + } else { + [self readCharacteristics:service]; + } +} + +- (void)peripheral:(CBPeripheral *)aPeripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic + error:(NSError *)error +{ + Q_UNUSED(aPeripheral) + + using namespace OSXBluetooth; + + Q_ASSERT_X(managerState != CentralManagerUpdating, + "-peripheral:didUpdateValueForCharacteristic:error:", + "invalid state"); + Q_ASSERT_X(peripheral, "-peripheral:didUpdateValueForCharacteristic:error:", + "invalid peripheral (nil)"); + + QT_BT_MAC_AUTORELEASEPOOL; + // 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); + + if (error) { + // Use NSLog, not qCDebug/qCWarning to log the actual error. + NSLog(@"-peripheral:didUpdateValueForCharacteristic:error:, failed with error %@", + error); + + if (!isDetailsDiscovery) { + // TODO: this can be something else in a future (if needed at all). + return; + } + } + + if (isDetailsDiscovery) { + // Test if we have any other characteristic to read yet. + CBCharacteristic *const next = [self nextCharacteristicForService:characteristic.service + startingFrom:characteristic properties:CBCharacteristicPropertyRead]; + if (next) + [peripheral readValueForCharacteristic:next]; + else + [self discoverDescriptors:characteristic.service]; + } else { + // TODO: this can be something else in a future (if needed at all). + } +} + +- (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) + + QT_BT_MAC_AUTORELEASEPOOL; + + using namespace OSXBluetooth; + + if (error) { + // Log the error using NSLog: + NSLog(@"-peripheral:didDiscoverDescriptorsForCharacteristic:error:, failed with error %@", + error); + // Probably, 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) + [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, "-peripheral:didUpdateValueForDescriptor:error:", + "invalid peripheral (nil)"); + + QT_BT_MAC_AUTORELEASEPOOL; + + using namespace OSXBluetooth; + + CBService *const service = descriptor.characteristic.service; + const QBluetoothUuid qtUuid(qt_uuid(service.UUID)); + const bool isDetailsDiscovery = servicesToDiscoverDetails.contains(qtUuid); + + if (error) { + // NSLog to log the actual error ... + NSLog(@"-peripheral:didUpdateValueForDescriptor:error:, failed with error %@", + error); + if (!isDetailsDiscovery) { + // TODO: probably will be required in a future. + return; + } + } + + if (isDetailsDiscovery) { + // Test if we have any other characteristic to read yet. + CBDescriptor *const next = [self nextDescriptorForCharacteristic:descriptor.characteristic + startingFrom:descriptor]; + if (next) { + [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) + return [peripheral readValueForDescriptor:[nextCh.descriptors objectAtIndex:0]]; + + nextCh = [self nextCharacteristicForService:ch.service + startingFrom:nextCh]; + } + + [self serviceDetailsDiscoveryFinished:service]; + } + } else { + // TODO: this can be something else in a future (if needed at all). + } } @end diff --git a/src/bluetooth/osx/osxbtcentralmanager_p.h b/src/bluetooth/osx/osxbtcentralmanager_p.h index 7ad51fb6..25faab78 100644 --- a/src/bluetooth/osx/osxbtcentralmanager_p.h +++ b/src/bluetooth/osx/osxbtcentralmanager_p.h @@ -65,13 +65,14 @@ class CentralManagerDelegate public: typedef QT_MANGLE_NAMESPACE(OSXBTCentralManager) ObjCCentralManager; typedef ObjCStrongReference<NSArray> LEServices; - typedef LEServices LECharacteristics; + typedef ObjCStrongReference<CBService> LEService; virtual ~CentralManagerDelegate(); virtual void LEnotSupported() = 0; virtual void connectSuccess() = 0; virtual void serviceDiscoveryFinished(LEServices services) = 0; + virtual void serviceDetailsDiscoveryFinished(LEService service) = 0; virtual void disconnected() = 0; // General errors. @@ -117,6 +118,8 @@ QT_END_NAMESPACE QT_PREPEND_NAMESPACE(OSXBluetooth)::ObjCStrongReference<NSMutableArray> servicesToVisitNext; // We'd like to avoid loops in a services' topology: QT_PREPEND_NAMESPACE(OSXBluetooth)::ObjCStrongReference<NSMutableSet> visitedServices; + + QT_PREPEND_NAMESPACE(QList)<QT_PREPEND_NAMESPACE(QBluetoothUuid)> servicesToDiscoverDetails; } - (id)initWithDelegate:(QT_PREPEND_NAMESPACE(OSXBluetooth)::CentralManagerDelegate *)aDelegate; @@ -128,6 +131,7 @@ QT_END_NAMESPACE - (void)disconnectFromDevice; - (void)discoverServices; +- (bool)discoverServiceDetails:(const QT_PREPEND_NAMESPACE(QBluetoothUuid) &)serviceUuid; @end diff --git a/src/bluetooth/osx/osxbtutility.mm b/src/bluetooth/osx/osxbtutility.mm index 69e94434..1b07e2cb 100644 --- a/src/bluetooth/osx/osxbtutility.mm +++ b/src/bluetooth/osx/osxbtutility.mm @@ -43,6 +43,7 @@ #include "osxbtutility_p.h" #include "qbluetoothuuid.h" +#include <QtCore/qendian.h> #include <QtCore/qstring.h> #ifndef QT_IOS_BLUETOOTH @@ -176,12 +177,10 @@ QBluetoothUuid qt_uuid(CBUUID *uuid) QT_BT_MAC_AUTORELEASEPOOL; if (uuid.data.length == 2) { - // TODO: this is .. UGLY :) - quint16 qtUuidData = 0; - const quint8 *const source = static_cast<const quint8 *>(uuid.data.bytes); - std::copy(source, source + 2, &qtUuidData); - - return QBluetoothUuid(qtUuidData); + // 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); @@ -223,6 +222,30 @@ ObjCStrongReference<CBUUID> cb_uuid(const QBluetoothUuid &qtUuid) 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; +} + } QT_END_NAMESPACE diff --git a/src/bluetooth/osx/osxbtutility_p.h b/src/bluetooth/osx/osxbtutility_p.h index fb7b9363..6e226a25 100644 --- a/src/bluetooth/osx/osxbtutility_p.h +++ b/src/bluetooth/osx/osxbtutility_p.h @@ -285,6 +285,9 @@ QString qt_error_string(IOReturn errorCode); 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); } // namespace OSXBluetooth diff --git a/src/bluetooth/qlowenergycontroller_osx.mm b/src/bluetooth/qlowenergycontroller_osx.mm index b576e76f..9a86c392 100644 --- a/src/bluetooth/qlowenergycontroller_osx.mm +++ b/src/bluetooth/qlowenergycontroller_osx.mm @@ -91,6 +91,7 @@ ServicePrivate qt_createLEService(QLowEnergyControllerPrivateOSX *controller, CB if (included) newService->type |= QLowEnergyService::IncludedService; + // TODO: isPrimary is ... always 'NO' - to be investigated. /* #if QT_MAC_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_9, __IPHONE_6_0) if (!cbService.isPrimary) { @@ -128,7 +129,8 @@ QLowEnergyControllerPrivateOSX::QLowEnergyControllerPrivateOSX(QLowEnergyControl isConnecting(false), lastError(QLowEnergyController::NoError), controllerState(QLowEnergyController::UnconnectedState), - addressType(QLowEnergyController::PublicAddress) + addressType(QLowEnergyController::PublicAddress), + lastValidHandle(0) // 0 == invalid. { // This is the "wrong" constructor - no valid device UUID to connect later. Q_ASSERT_X(q, "QLowEnergyControllerPrivate", "invalid q_ptr (null)"); @@ -149,7 +151,8 @@ QLowEnergyControllerPrivateOSX::QLowEnergyControllerPrivateOSX(QLowEnergyControl isConnecting(false), lastError(QLowEnergyController::NoError), controllerState(QLowEnergyController::UnconnectedState), - addressType(QLowEnergyController::PublicAddress) + addressType(QLowEnergyController::PublicAddress), + lastValidHandle(0) // 0 == invalid. { Q_ASSERT_X(q, "QLowEnergyControllerPrivateOSX", "invalid q_ptr (null)"); centralManager.reset([[ObjCCentralManager alloc] initWithDelegate:this]); @@ -284,6 +287,64 @@ void QLowEnergyControllerPrivateOSX::serviceDiscoveryFinished(LEServices service QMetaObject::invokeMethod(q_ptr, "discoveryFinished", Qt::QueuedConnection); } +void QLowEnergyControllerPrivateOSX::serviceDetailsDiscoveryFinished(LEService service) +{ + if (!service) { + qCDebug(QT_BT_OSX) << "QLowEnergyControllerPrivateOSX::serviceDetailsDiscoveryFinished(), " + "invalid service (nil)"; + return; + } + + QT_BT_MAC_AUTORELEASEPOOL; + + const QBluetoothUuid qtUuid(OSXBluetooth::qt_uuid(service.data().UUID)); + if (!discoveredServices.contains(qtUuid)) { + qCDebug(QT_BT_OSX) << "QLowEnergyControllerPrivateOSX::serviceDetailsDiscoveryFinished(), " + "unknown service uuid: " << qtUuid; + return; + } + + ServicePrivate qtService(discoveredServices.value(qtUuid)); + qtService->startHandle = ++lastValidHandle; + // Now we iterate on characteristics and descriptors (if any). + NSArray *const cs = service.data().characteristics; + if (cs && cs.count) { + QHash<QLowEnergyHandle, QLowEnergyServicePrivate::CharData> charList; + // That's not a real handle - just a key into a hash map. + for (CBCharacteristic *c in cs) { + QLowEnergyServicePrivate::CharData newChar = {}; + newChar.uuid = OSXBluetooth::qt_uuid(c.UUID); + // CBCharacteristicProperty enum has the same values as Qt + + // a couple of values above 'extended' we do not support yet - mask them out. + // All other possible enumerators are the same: + const int cbProps = c.properties & 0xff; + newChar.properties = static_cast<QLowEnergyCharacteristic::PropertyTypes>(cbProps); + newChar.value = OSXBluetooth::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) { + QLowEnergyServicePrivate::DescData newDesc = {}; + newDesc.uuid = OSXBluetooth::qt_uuid(d.UUID); + newDesc.value = OSXBluetooth::qt_bytearray(d.value); + descList[++lastValidHandle] = newDesc; + } + + newChar.descriptorList = descList; + } + + charList[newChar.valueHandle] = newChar; + } + + qtService->characteristicList = charList; + } + + qtService->endHandle = lastValidHandle; + qtService->stateChanged(QLowEnergyService::ServiceDiscovered); +} + void QLowEnergyControllerPrivateOSX::disconnected() { controllerState = QLowEnergyController::UnconnectedState; @@ -324,8 +385,16 @@ void QLowEnergyControllerPrivateOSX::error(const QBluetoothUuid &serviceUuid, QLowEnergyController::Error errorCode) { // Errors reported while discovering service details etc. - Q_UNUSED(serviceUuid) - Q_UNUSED(errorCode) + Q_UNUSED(errorCode) // TODO: setError? + + // We failed to discover any characteristics/descriptors. + if (discoveredServices.contains(serviceUuid)) { + ServicePrivate qtService(discoveredServices.value(serviceUuid)); + qtService->stateChanged(QLowEnergyService::InvalidService); + } else { + qCDebug(QT_BT_OSX) << "QLowEnergyControllerPrivateOSX::error(), " + "error reported for unknown service "<<serviceUuid; + } } void QLowEnergyControllerPrivateOSX::connectToDevice() @@ -377,7 +446,29 @@ void QLowEnergyControllerPrivateOSX::discoverServices() void QLowEnergyControllerPrivateOSX::discoverServiceDetails(const QBluetoothUuid &serviceUuid) { - Q_UNUSED(serviceUuid); + Q_ASSERT_X(isValid(), "discoverServiceDetails", "invalid private controller"); + + if (controllerState != QLowEnergyController::DiscoveredState) { + qCWarning(QT_BT_OSX) << "QLowEnergyControllerPrivateOSX::discoverServiceDetails(), " + "can not discover service details in the current state, " + "QLowEnergyController::DiscoveredState is expected"; + return; + } + + if (!discoveredServices.contains(serviceUuid)) { + qCWarning(QT_BT_OSX) << "QLowEnergyControllerPrivateOSX::discoverServiceDetails(), " + "unknown service: " << serviceUuid; + return; + } + + ServicePrivate qtService(discoveredServices.value(serviceUuid)); + if ([centralManager discoverServiceDetails:serviceUuid]) { + qtService->stateChanged(QLowEnergyService::DiscoveringServices); + } else { + // The error is returned by CentralManager - no + // service with a given UUID found on a peripheral. + qtService->stateChanged(QLowEnergyService::InvalidService); + } } void QLowEnergyControllerPrivateOSX::setErrorDescription(QLowEnergyController::Error errorCode) @@ -413,6 +504,7 @@ void QLowEnergyControllerPrivateOSX::invalidateServices() service->setState(QLowEnergyService::InvalidService); } + lastValidHandle = 0; discoveredServices.clear(); } diff --git a/src/bluetooth/qlowenergycontroller_osx_p.h b/src/bluetooth/qlowenergycontroller_osx_p.h index 36257e02..f9764c03 100644 --- a/src/bluetooth/qlowenergycontroller_osx_p.h +++ b/src/bluetooth/qlowenergycontroller_osx_p.h @@ -66,6 +66,7 @@ private: void connectSuccess() Q_DECL_OVERRIDE; void serviceDiscoveryFinished(LEServices services) Q_DECL_OVERRIDE; + void serviceDetailsDiscoveryFinished(LEService service) Q_DECL_OVERRIDE; void disconnected() Q_DECL_OVERRIDE; void error(QLowEnergyController::Error errorCode) Q_DECL_OVERRIDE; void error(const QBluetoothUuid &serviceUuid, @@ -103,6 +104,13 @@ private: typedef ServiceMap::const_iterator ConstServiceIterator; typedef ServiceMap::iterator ServiceIterator; ServiceMap discoveredServices; + + // While Core Bluetooth has _startHandle/_endHandle for + // CBServices, this information is not a part of a public + // API and can not be used. Instead we have to 'emulate' + // these handles using something that looks like/works like + // handles: + QLowEnergyHandle lastValidHandle; }; QT_END_NAMESPACE |