summaryrefslogtreecommitdiffstats
path: root/src/bluetooth/osx/osxbtcentralmanager.mm
diff options
context:
space:
mode:
Diffstat (limited to 'src/bluetooth/osx/osxbtcentralmanager.mm')
-rw-r--r--src/bluetooth/osx/osxbtcentralmanager.mm226
1 files changed, 191 insertions, 35 deletions
diff --git a/src/bluetooth/osx/osxbtcentralmanager.mm b/src/bluetooth/osx/osxbtcentralmanager.mm
index 70473f1f..9254bd98 100644
--- a/src/bluetooth/osx/osxbtcentralmanager.mm
+++ b/src/bluetooth/osx/osxbtcentralmanager.mm
@@ -79,8 +79,21 @@ NSUInteger qt_countGATTEntries(CBService *service)
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*/);
}
+} // namespace OSXBluetooth
+
QT_END_NAMESPACE
@@ -115,9 +128,11 @@ QT_END_NAMESPACE
- (id)initWith:(OSXBluetooth::LECBManagerNotifier *)aNotifier
{
+ using namespace OSXBluetooth;
+
if (self = [super init]) {
manager = nil;
- managerState = OSXBluetooth::CentralManagerIdle;
+ managerState = CentralManagerIdle;
disconnectPending = false;
peripheral = nil;
notifier = aNotifier;
@@ -125,6 +140,17 @@ QT_END_NAMESPACE
lastValidHandle = 0;
requestPending = false;
currentReadHandle = 0;
+ timeoutType = OperationTimeout::none;
+
+ 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;
@@ -153,6 +179,63 @@ QT_END_NAMESPACE
[super dealloc];
}
+- (void)watchAfter:(id)object timeout:(OSXBluetooth::OperationTimeout)type
+{
+ using namespace OSXBluetooth;
+
+ objectUnderWatch = object;
+ timeoutType = type;
+ [timeoutWatchdog cancelTimer];
+ timeoutWatchdog.reset([[GCDTimerObjC alloc] initWithDelegate:self]);
+ [timeoutWatchdog startWithTimeout:timeoutMS step:200];
+}
+
+- (void)stopWatchdog
+{
+ [timeoutWatchdog cancelTimer];
+ objectUnderWatch = nil;
+ timeoutType = OSXBluetooth::OperationTimeout::none;
+}
+
+- (void)timeout
+{
+ using namespace OSXBluetooth;
+
+ Q_ASSERT(objectUnderWatch);
+ NSLog(@"Timeout caused by: %@", objectUnderWatch);
+ const ObjCStrongReference<NSError> nsError(qt_timeoutNSError(timeoutType));
+ switch (timeoutType) {
+ case OperationTimeout::serviceDiscovery:
+ qCWarning(QT_BT_OSX, "Timeout in services discovery");
+ [self peripheral:peripheral didDiscoverServices:nsError];
+ break;
+ case OperationTimeout::includedServicesDiscovery:
+ qCWarning(QT_BT_OSX, "Timeout in included services discovery");
+ [self peripheral:peripheral didDiscoverIncludedServicesForService:objectUnderWatch error:nsError];
+ break;
+ case OperationTimeout::characteristicsDiscovery:
+ qCWarning(QT_BT_OSX, "Timeout in characteristics discovery");
+ [self peripheral:peripheral didDiscoverCharacteristicsForService:objectUnderWatch error:nsError];
+ break;
+ case OperationTimeout::characteristicRead:
+ qCWarning(QT_BT_OSX, "Timeout while reading a characteristic");
+ [self peripheral:peripheral didUpdateValueForCharacteristic:objectUnderWatch error:nsError];
+ break;
+ case OperationTimeout::descriptorsDiscovery:
+ qCWarning(QT_BT_OSX, "Timeout in descriptors discovery");
+ [self peripheral:peripheral didDiscoverDescriptorsForCharacteristic:objectUnderWatch error:nsError];
+ break;
+ case OperationTimeout::descriptorRead:
+ qCWarning(QT_BT_OSX, "Timeout while reading a descriptor");
+ [self peripheral:peripheral didUpdateValueForDescriptor:objectUnderWatch error:nsError];
+ break;
+ case OperationTimeout::characteristicWrite:
+ qCWarning(QT_BT_OSX, "Timeout while writing a characteristic with response");
+ [self peripheral:peripheral didWriteValueForCharacteristic:objectUnderWatch error:nsError];
+ default:;
+ }
+}
+
- (void)connectToDevice:(const QBluetoothUuid &)aDeviceUuid
{
disconnectPending = false; // Cancel the previous disconnect if any.
@@ -235,10 +318,11 @@ QT_END_NAMESPACE
- (void)connectToPeripheral
{
+ using namespace OSXBluetooth;
+
Q_ASSERT_X(manager, Q_FUNC_INFO, "invalid central manager (nil)");
Q_ASSERT_X(peripheral, Q_FUNC_INFO, "invalid peripheral (nil)");
- Q_ASSERT_X(managerState == OSXBluetooth::CentralManagerIdle,
- Q_FUNC_INFO, "invalid state");
+ Q_ASSERT_X(managerState == CentralManagerIdle, Q_FUNC_INFO, "invalid state");
// The state is still the same - connecting.
if ([self isConnected]) {
@@ -247,7 +331,7 @@ QT_END_NAMESPACE
emit notifier->connected();
} else {
qCDebug(QT_BT_OSX) << "trying to connect";
- managerState = OSXBluetooth::CentralManagerConnecting;
+ managerState = CentralManagerConnecting;
[manager connectPeripheral:peripheral options:nil];
}
}
@@ -292,9 +376,10 @@ QT_END_NAMESPACE
- (void)discoverServices
{
+ using namespace OSXBluetooth;
+
Q_ASSERT_X(peripheral, Q_FUNC_INFO, "invalid peripheral (nil)");
- Q_ASSERT_X(managerState == OSXBluetooth::CentralManagerIdle,
- Q_FUNC_INFO, "invalid state");
+ Q_ASSERT_X(managerState == CentralManagerIdle, Q_FUNC_INFO, "invalid state");
// From Apple's docs:
//
@@ -304,7 +389,8 @@ QT_END_NAMESPACE
//
// ... but we'd like to have them all:
[peripheral setDelegate:self];
- managerState = OSXBluetooth::CentralManagerDiscovering;
+ managerState = CentralManagerDiscovering;
+ [self watchAfter:peripheral timeout:OperationTimeout::serviceDiscovery];
[peripheral discoverServices:nil];
}
@@ -333,6 +419,7 @@ QT_END_NAMESPACE
CBService *const s = [services objectAtIndex:currentService];
[visitedServices addObject:s];
managerState = CentralManagerDiscovering;
+ [self watchAfter:s timeout:OperationTimeout::includedServicesDiscovery];
[peripheral discoverIncludedServices:nil forService:s];
}
}
@@ -359,6 +446,7 @@ QT_END_NAMESPACE
if (CBService *const service = [self serviceForUUID:serviceUuid]) {
servicesToDiscoverDetails.append(serviceUuid);
+ [self watchAfter:service timeout:OperationTimeout::characteristicsDiscovery];
[peripheral discoverCharacteristics:nil forService:service];
return;
}
@@ -366,10 +454,8 @@ QT_END_NAMESPACE
qCWarning(QT_BT_OSX) << "unknown service uuid"
<< serviceUuid;
- if (notifier) {
- emit notifier->CBManagerError(serviceUuid,
- QLowEnergyService::UnknownError);
- }
+ if (notifier)
+ emit notifier->CBManagerError(serviceUuid, QLowEnergyService::UnknownError);
}
- (void)readCharacteristics:(CBService *)service
@@ -391,8 +477,10 @@ QT_END_NAMESPACE
NSArray *const cs = service.characteristics;
for (CBCharacteristic *c in cs) {
- if (c.properties & CBCharacteristicPropertyRead)
+ if (c.properties & CBCharacteristicPropertyRead) {
+ [self watchAfter:c timeout:OperationTimeout::characteristicRead];
return [peripheral readValueForCharacteristic:c];
+ }
}
// No readable properties? Discover descriptors then:
@@ -418,15 +506,18 @@ QT_END_NAMESPACE
[self serviceDetailsDiscoveryFinished:service];
} else {
// Start from 0 and continue in the callback.
- [peripheral discoverDescriptorsForCharacteristic:[service.characteristics objectAtIndex:0]];
+ CBCharacteristic *ch = [service.characteristics objectAtIndex:0];
+ [self watchAfter:ch timeout:OperationTimeout::descriptorsDiscovery];
+ [peripheral discoverDescriptorsForCharacteristic:ch];
}
}
- (void)readDescriptors:(CBService *)service
{
+ using namespace OSXBluetooth;
+
Q_ASSERT_X(service, Q_FUNC_INFO, "invalid service (nil)");
- Q_ASSERT_X(managerState != OSXBluetooth::CentralManagerUpdating,
- Q_FUNC_INFO, "invalid state");
+ Q_ASSERT_X(managerState != CentralManagerUpdating, Q_FUNC_INFO, "invalid state");
Q_ASSERT_X(peripheral, Q_FUNC_INFO, "invalid peripheral (nil)");
QT_BT_MAC_AUTORELEASEPOOL;
@@ -435,8 +526,11 @@ QT_END_NAMESPACE
// 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)
- return [peripheral readValueForDescriptor:[c.descriptors objectAtIndex:0]];
+ 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.
@@ -571,6 +665,8 @@ QT_END_NAMESPACE
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)) {
@@ -581,6 +677,7 @@ QT_END_NAMESPACE
requestPending = true;
currentReadHandle = request.handle;
+ // Timeouts: see the comment above (CharRead).
[peripheral readValueForDescriptor:descMap[request.handle]];
}
}
@@ -657,6 +754,7 @@ QT_END_NAMESPACE
return [self performNextRequest];
requestPending = true;
+ [self watchAfter:characteristic timeout:OperationTimeout::characteristicWrite];
[peripheral writeValue:data.data() forCharacteristic:characteristic
type:CBCharacteristicWriteWithResponse];
} else {
@@ -1021,6 +1119,7 @@ QT_END_NAMESPACE
charMap.clear();
descMap.clear();
currentReadHandle = 0;
+ [self stopWatchdog];
// TODO: also serviceToVisit/VisitNext and visitedServices ?
}
@@ -1030,6 +1129,9 @@ QT_END_NAMESPACE
{
using namespace OSXBluetooth;
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wunguarded-availability-new"
+
const auto state = central.state;
#if QT_IOS_PLATFORM_SDK_EQUAL_OR_ABOVE(__IPHONE_10_0) || QT_OSX_PLATFORM_SDK_EQUAL_OR_ABOVE(__MAC_10_13)
if (state == CBManagerStateUnknown
@@ -1072,12 +1174,14 @@ QT_END_NAMESPACE
#else
if (state == CBCentralManagerStatePoweredOff) {
#endif
- managerState = CentralManagerIdle;
+
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)
@@ -1099,6 +1203,8 @@ QT_END_NAMESPACE
// We actually handled all known states, but .. Core Bluetooth can change?
Q_ASSERT_X(0, Q_FUNC_INFO, "invalid centra's state");
}
+
+#pragma clang diagnostic pop
}
- (void)centralManager:(CBCentralManager *)central didConnectPeripheral:(CBPeripheral *)aPeripheral
@@ -1162,10 +1268,16 @@ QT_END_NAMESPACE
Q_UNUSED(aPeripheral)
if (managerState != OSXBluetooth::CentralManagerDiscovering) {
- // Canceled by -disconnectFromDevice.
+ // Canceled by -disconnectFromDevice, or as a result of a timeout.
+ return;
+ }
+
+ if (objectUnderWatch != aPeripheral) {
+ // Timed out.
return;
}
+ [self stopWatchdog];
managerState = OSXBluetooth::CentralManagerIdle;
if (error) {
@@ -1173,9 +1285,9 @@ QT_END_NAMESPACE
// TODO: better error mapping required.
if (notifier)
emit notifier->CBManagerError(QLowEnergyController::UnknownError);
- } else {
- [self discoverIncludedServices];
}
+
+ [self discoverIncludedServices];
}
@@ -1191,10 +1303,16 @@ QT_END_NAMESPACE
return;
}
+ if (service != objectUnderWatch) {
+ // Timed out.
+ return;
+ }
+
QT_BT_MAC_AUTORELEASEPOOL;
Q_ASSERT_X(peripheral, Q_FUNC_INFO, "invalid peripheral (nil)");
+ [self stopWatchdog];
managerState = CentralManagerIdle;
if (error) {
@@ -1217,6 +1335,7 @@ QT_END_NAMESPACE
// Continue with discovery ...
[visitedServices addObject:s];
managerState = CentralManagerDiscovering;
+ [self watchAfter:s timeout:OperationTimeout::includedServicesDiscovery];
return [peripheral discoverIncludedServices:nil forService:s];
}
}
@@ -1232,6 +1351,7 @@ QT_END_NAMESPACE
if (![visitedServices containsObject:s]) {
[visitedServices addObject:s];
managerState = CentralManagerDiscovering;
+ [self watchAfter:s timeout:OperationTimeout::includedServicesDiscovery];
return [peripheral discoverIncludedServices:nil forService:s];
}
}
@@ -1260,6 +1380,13 @@ QT_END_NAMESPACE
return;
}
+ if (service != objectUnderWatch) {
+ // Timed out already?
+ return;
+ }
+
+ [self stopWatchdog];
+
using namespace OSXBluetooth;
Q_ASSERT_X(managerState != CentralManagerUpdating, Q_FUNC_INFO, "invalid state");
@@ -1269,9 +1396,9 @@ QT_END_NAMESPACE
// 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);
- } else {
- [self readCharacteristics:service];
}
+
+ [self readCharacteristics:service];
}
- (void)peripheral:(CBPeripheral *)aPeripheral
@@ -1285,6 +1412,10 @@ QT_END_NAMESPACE
return;
}
+ const bool readMatch = characteristic == objectUnderWatch;
+ if (readMatch)
+ [self stopWatchdog];
+
using namespace OSXBluetooth;
Q_ASSERT_X(managerState != CentralManagerUpdating, Q_FUNC_INFO, "invalid state");
@@ -1311,13 +1442,17 @@ QT_END_NAMESPACE
}
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];
+ 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) -
@@ -1360,6 +1495,11 @@ QT_END_NAMESPACE
QT_BT_MAC_AUTORELEASEPOOL;
+ if (characteristic != objectUnderWatch)
+ return;
+
+ [self stopWatchdog];
+
using namespace OSXBluetooth;
if (error) {
@@ -1370,10 +1510,12 @@ QT_END_NAMESPACE
// Do we have more characteristics on this service to discover descriptors?
CBCharacteristic *const next = [self nextCharacteristicForService:characteristic.service
startingFrom:characteristic];
- if (next)
+ if (next) {
+ [self watchAfter:next timeout:OperationTimeout::descriptorsDiscovery];
[peripheral discoverDescriptorsForCharacteristic:next];
- else
+ } else {
[self readDescriptors:characteristic.service];
+ }
}
- (void)peripheral:(CBPeripheral *)aPeripheral
@@ -1391,6 +1533,11 @@ QT_END_NAMESPACE
QT_BT_MAC_AUTORELEASEPOOL;
+ if (descriptor != objectUnderWatch)
+ return;
+
+ [self stopWatchdog];
+
using namespace OSXBluetooth;
CBService *const service = descriptor.characteristic.service;
@@ -1417,6 +1564,7 @@ QT_END_NAMESPACE
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
@@ -1426,8 +1574,11 @@ QT_END_NAMESPACE
CBCharacteristic *nextCh = [self nextCharacteristicForService:ch.service
startingFrom:ch];
while (nextCh) {
- if (nextCh.descriptors && nextCh.descriptors.count)
- return [peripheral readValueForDescriptor:[nextCh.descriptors objectAtIndex:0]];
+ 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];
@@ -1474,6 +1625,10 @@ QT_END_NAMESPACE
QT_BT_MAC_AUTORELEASEPOOL;
+ if (characteristic != objectUnderWatch)
+ return;
+
+ [self stopWatchdog];
requestPending = false;
@@ -1569,9 +1724,10 @@ QT_END_NAMESPACE
if (notifier) {
notifier->disconnect();
notifier->deleteLater();
- notifier = 0;
+ notifier = nullptr;
}
+ [self stopWatchdog];
[self disconnectFromDevice];
}