1

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?

  • `test = [TEST alloc];` Could this be set as a var of your class? And check that i's always "alive" during your test? I'm wondering if if not deallocated too early. – Larme Dec 30 '19 at 13:00
  • @Larme what's the quickest way to check if the object is still 'alive'? I checked whether I can still access the class of the object (test.class)... and based on the result the object still seems 'alive' to me. – TheQuestioner Dec 30 '19 at 13:11
  • Make it a singleton (look for sharedInstance Objective-C codes), check if it works. If so, it might be due to the releasing too soon. – Larme Dec 30 '19 at 13:20
  • Update: you are probably correct. The problems disappeared when I ran a quick test using a variable declared outside of the method for storing 'test'. Conclusion: apparently the block only gets a weak reference to 'test' and can't prevent it from being deallocated prematurely. – TheQuestioner Dec 30 '19 at 13:39
  • It's a common issue that a `CBCentralManager` is deallocated too soon, making its delegate method not called. There are a few questions about that in SO. Make the block retain it maybe? – Larme Dec 30 '19 at 13:41
  • You should probably avoid using an `NSThread` calls and definitely avoid `sleepForTimeInterval`. Even if you are not running on the main *queue* you may be running on the main *thread*. As GCD can use any available thread, including the main thread, for work on a queue other than the main queue. You should use an `NSTimer` To terminate your discovery process and initiate your callback. – Paulw11 Dec 30 '19 at 19:42

0 Answers0