5

First of all the question what is the best way of using core bluetooth in the central role to send data to a bluetooth LE devices. The data need to be processed and that takes enough time to cause problems on the UI thread if it runs on it. The user will initiate the process with the phone app open and then either keep using the app or close the app and expect the data to continue sending to the device.

I have found 2 really bad ways of doing it which seem to work

  • Put the bluetooth CBCentralManager objet on the main queue and risk blocking the UI
  • Ignore the indications from the iOS bluetooth stack that its not ready to transmit and risk loosing data.

This seems to have its roots in both iOS threading/dispatch queues as well as iOS Bluetooth internals.

The bluetooth LE iOS application connects to a bluetooth LE device as central role. The CBCentralManager is initialized according to apples documentation. The queue defined as:

The dispatch queue to use to dispatch the central role events. If the value is nil, the central manager dispatches central role events using the main queue.

As suggested by vladiulianbogdan answer to Swift: Choose queue for Bluetooth Central manager we should be creating a serial queue for the CBCentralManager. This seems to make sense and for a while I was following this advice. Also allprog comment to Swift CoreBluetooth: Should CentralManager run in a separate thread suggests that the main queue will be suspended but other queues will not, which is the opposite of what I am seeing.

While using a serial queue for bluetooth, and using a different one than the main thread would be preferable. There is a problem: The callback:

    -(void)peripheralIsReadyToSendWriteWithoutResponse:(CBPeripheral *)peripheral
    {
        [self sendNextBluetoothLePacket];
    }

stop getting called. There is another way to check if the peripheral is ready to send more data, CBPeripheral has a member variable canSendWriteWithoutResponse which returns true if its ok to send. This variable also begins retuning false and never goes back to true.

I found this comment from Sandeep Bhandari which says that all of the queue threads are stopped when the app goes in the background unless they are one of the background modes provided by apple. Biniou found that he was able to solve his core bluetooth background issue by initializing in a view controller instead of the app delegate. This does not make sense to me.

My app does have the core-bluetooth background mode selected in its info.plist so it should quality as one of those background modes. What I find is that when my app goes in the background the app does continue process data. I see log messages from polling loops that run every 100 milliseconds.

If I trigger bluetooth LE writes from those polling loops I am able to keep sending data. The problem is I am unable to determine a safe rate to send the data and either its very slow or data is sometimes lost.

Im not sure how to best deal with this. Any suggestions would be appreciated. It seems like no matter what I do when I go in to the background I loose my ability to determine if its safe to send data.

I see this comment that indicates that the only solution would be to change how the bluetooth device connects to the phone, is this the case? Im not sure changing the hardware is an option for me at this point.

The ideal solution would be to find a way to put CBCentralManager on its own serial queue but create that queue in such a way that the queue was not stopped when the app goes in to the background. If someone knows how to do that I believe it would solve my problem.

The way my current code is goes like this. When the bluetooth service is created in the applicationDidFinishLaunchingWithOptions callback to my AppDelegate

    self.cm = [[CBCentralManager alloc] initWithDelegate:self
                                                   queue:nil
                                                 options:@{CBCentralManagerOptionShowPowerAlertKey:@(0)}];

Or with the serial queue which should work but is not

    dispatch_queue_t bt_queue = dispatch_queue_create("BT_queue", 0);
    self.cm = [[CBCentralManager alloc] initWithDelegate:self
                                                   queue:bt_queue
                                                 options:@{CBCentralManagerOptionShowPowerAlertKey:@(0)}];

When its time to send some data I use one of these

        [self.cbPeripheral writeValue:data
                    forCharacteristic:self.rxCharacteristic
                                 type:CBCharacteristicWriteWithoutResponse];

If I just keep calling this writeValue with no delay in between without trying to check if its safe to send data. It will eventually fail.

Extra background execution time is requested once the connection is established with this code

    - (void)   centralManager:(CBCentralManager *)central
     didConnectPeripheral:(CBPeripheral *)peripheral
    {
        UIApplication *app = [UIApplication sharedApplication];
        if (self.globalBackgroundTask != UIBackgroundTaskInvalid) {
            [app endBackgroundTask:self.globalBackgroundTask];
            self.globalBackgroundTask = UIBackgroundTaskInvalid;
        }
        self.globalBackgroundTask = [app beginBackgroundTaskWithExpirationHandler:^{
            [app endBackgroundTask:_globalBackgroundTask];
            _globalBackgroundTask = UIBackgroundTaskInvalid;
        }];

Any thoughts on this or suggestions as to how I could give CBCentralManager a queue that was not on the UI thread and would not get shut down when the app goes in to the background would be greatly appreciated. If thats not possible I need to try and pick one of the workarounds.

Mahmoud Adam
  • 5,772
  • 5
  • 41
  • 62
Marc
  • 1,159
  • 17
  • 31
  • Are you requesting `globalBackgroundTask` on main thread? – Eugene Dudnyk Jun 18 '19 at 18:27
  • I had suspected that could have been the problem. I did try wrapping that up in a explicit dispatch_async on the queue I created and did not have any luck with that. Thanks – Marc Jun 19 '19 at 18:36
  • It seems that the issue with I/O events not being processed is related to run loop of the background queue. Try to play with `CFRunLoopPerformBlock(CFRunLoopGetCurrent(), kCFRunLoopCommonModes, ^{ [self.cbPeripheral writeValue:data forCharacteristic:self.rxCharacteristic type:CBCharacteristicWriteWithoutResponse]; } )` – Eugene Dudnyk Jun 19 '19 at 22:03

0 Answers0