How to use iOS CBCentralManager with a serial queue for an app that goes in to the background?

First of all the question what is the best way of using core bluetooth in the central role to send data to send data to a bluetooth LE devices. The data it sends needs 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.


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.


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.


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.

Replies

Hi. Sorry for joining the party 2 years later. I'm also experiencing the same issue if a serial dispatch queue is used. It's fairly easy to trigger the issue as well. When using CBCharacteristicWriteWithoutResponse you want to check that it's possible to do writes:

if ([peripheral canSendWriteWithoutResponse]) {

If the function above returns NO you'll have to wait until the delegate function peripheralIsReadyToSendWriteWithoutResponse is called before trying again.

However if a serial dispatch queue is used and the app is put in background mode and then back to foreground mode the function canSendWriteWithoutResponse will most of the times (not always) respond NO. In those cases the delegate method peripheralIsReadyToSendWriteWithoutResponse is also never called.

Sending writes with responses works fine though even after the application comes from background. So the conclusion is that there's something fishy in the closed source of canSendWriteWithoutResponse when running on a serial dispatch queue.

Using a global queue doesn't give the same issue, however it's not wanted to run data transfer concurrently.

Post not yet marked as solved Up vote reply of lman Down vote reply of lman