I am trying to write some code to scan for bluetooth devices for a while, and then return the array of discovered peripherals through a block-based callback. Blocking code should not be an issue as the code will be called asynchronously.
After reading up on the API documentation my initial plan of attack was to write an implementation for CBCentralManagerDelegate, use an init method to give it a block-based callback to call once the CBManagerState is PoweredOn, and then initialize this class with a callback that triggers the scanning and extracts the discovered Peripherals.
The issue is... it doesn't work. Except when it does. Now I could work out a workaround to reach my goal, but for the sake of learning and understanding I am very interested in where exactly the issue originates from.
typedef void (^SomeBlock)(CBCentralManager*);
@interface TEST : NSObject <CBCentralManagerDelegate>
@property CBCentralManager* manager;
@property SomeBlock onPoweredOn;
@property NSMutableArray<CBPeripheral*>* peripherals;
- (void) init: (SomeBlock) onPoweredOn;
- (void) startScan;
- (void) stopScan;
@end
@implementation TEST
- (void) init: (SomeBlock) onPoweredOn {
NSLog(@"%@", @"init");
self.onPoweredOn = onPoweredOn;
self.manager = [CBCentralManager alloc];
dispatch_queue_attr_t attr = DISPATCH_QUEUE_CONCURRENT;
dispatch_queue_t queue =dispatch_queue_create("BTManagerHandler", attr);
self.manager = [self.manager initWithDelegate: self queue: queue];
}
- (void) startScan {
NSLog(@"%@", @"startScan");
[self.manager scanForPeripheralsWithServices: nil options: nil];
}
- (void) stopScan {
NSLog(@"%@", @"stopScan ");
[self.manager stopScan];
}
- (void) centralManagerDidUpdateState: (nonnull CBCentralManager *) manager {
NSLog(@"%@", @"centralManagerDidUpdateState:");
switch (manager.state) {
case CBManagerStateUnknown:
NSLog(@"%@", @"CBManagerStateUnknown:");
break;
case CBManagerStateResetting:
NSLog(@"%@", @"CBManagerStateResetting:");
break;
case CBManagerStateUnsupported:
NSLog(@"%@", @"CBManagerStateUnsupported:");
break;
case CBManagerStateUnauthorized:
NSLog(@"%@", @"CBManagerStateUnauthorized:");
break;
case CBManagerStatePoweredOff:
NSLog(@"%@", @"CBManagerStatePoweredOff:");
break;
case CBManagerStatePoweredOn:
NSLog(@"%@", @"CBManagerStatePoweredOn:");
if (self.onPoweredOn != nil) self.onPoweredOn(manager);
break;
}
}
- (void) centralManager: (nonnull CBCentralManager*) central didDiscoverPeripheral: (nonnull CBPeripheral*) peripheral advertisementData: (nonnull NSDictionary<NSString*, id>*) advertisementData RSSI: (nonnull NSNumber*) RSSI {
NSLog(@"%@", @"centralManager:didDiscoverPeripheral:advertisementData:RSSI:");
if (self.peripherals == nil) self.peripherals = [NSMutableArray array];
for (CBPeripheral* _peripheral in self.peripherals) {
if (peripheral.identifier == _peripheral.identifier) return;
}
[self.peripherals addObject: peripheral];
}
@end
+ (void) discoverDevices {
TEST* test = nil;
@try {
test = [TEST alloc];
SomeBlock onPoweredOn = ^(CBCentralManager* manager) {
NSLog(@"%@", @"_onPoweredOn_");
[test startScan];
[NSThread sleepForTimeInterval: 10.0];
[managerHandler stopScan];
NSArray<CBPeripheral*>* discoveredPeripherals = managerHandler.peripherals;
// do stuff with discoveredPeripherals
};
[test init: onPoweredOn];
} @catch(NSException* e) {
// exception handling
} @finally {
// cleanup
}
}
I would expect the above code to work, but it doesn't. The 'onPoweredOn' callback and the 'startScan' method are called correctly, but the 'centralManager:didDiscoverPeripheral:advertisementData:RSSI:' method is never called.
After some trial and error I found that the following works:
+ (void) discoverDevices {
TEST* test = nil;
@try {
test = [TEST alloc];
SomeBlock onPoweredOn = ^(CBCentralManager* manager) {
NSLog(@"%@", @"_onPoweredOn_");
[test startScan];
};
[test init: onPoweredOn];
[NSThread sleepForTimeInterval: 10.0];
[managerHandler stopScan];
NSArray<CBPeripheral*>* discoveredPeripherals = managerHandler.peripherals;
// do stuff with discoveredPeripherals
} @catch(NSException* e) {
// exception handling
} @finally {
// cleanup
}
}
After some more trial and error I narrowed it down to one line of code:
+ (void) discoverDevices {
TEST* test = nil;
@try {
test = [TEST alloc];
SomeBlock onPoweredOn = ^(CBCentralManager* manager) {
NSLog(@"%@", @"_onPoweredOn_");
[test startScan];
[NSThread sleepForTimeInterval: 10.0]; // <<=== this line! ===
};
[test init: onPoweredOn];
[NSThread sleepForTimeInterval: 10.0];
[managerHandler stopScan];
NSArray<CBPeripheral*>* discoveredPeripherals = managerHandler.peripherals;
// do stuff with discoveredPeripherals
} @catch(NSException* e) {
// exception handling
} @finally {
// cleanup
}
}
This suggests that that using a [NSThread sleepForTimeInterval:] blocks the discovery of bluetooth devices... but tat seems illogical to me because the same code works without the block-based callback:
+ (void) discoverDevices {
TEST* test = nil;
@try {
test = [TEST alloc];
[test init: nil];
[NSThread sleepForTimeInterval: 1.0];
[test startScan];
[NSThread sleepForTimeInterval: 10.0];
[managerHandler stopScan];
NSArray<CBPeripheral*>* discoveredPeripherals = managerHandler.peripherals;
// do stuff with discoveredPeripherals
} @catch(NSException* e) {
// exception handling
} @finally {
// cleanup
}
}
Conclusion: combining CBCentralManager, block-based callbacks and [NSThread sleepForTimeInterval:] leads to unexpected behaviour?? but why? what's so special about this specific combination?