CoreBluetooth - Gracefully Handling Disconnects

I’ve been working with CoreBluetooth over the last year while building an app for the goTenna (a BTLE enabled device). This has been my first time working with both BTLE and CoreBluetooth and it’s been quite a ride!

After figuring out how to connect and communicate with goTenna effectively, we quickly started observing intermittent disconnections. These disconnections took form in the following ways:

  • Immediately following a connection being established (no data writen or read yet)
  • Immediately following writing data to our peripheral (so, given a set of packets, the first one that goes out would not reach the peripheral and the connection would drop before anymore could be sent)
  • In the middle of an established connection when there was neither data coming from or being written to the peripheral (but had been successfully at earlier points in the session.)

Not knowing what was causing this. We started down a path of research and troubleshooting.

The Suspects

Usage of CoreBluetooth API

Let’s face it. As in life, when facing a problem, the first and easiest thing you can change is yourself. So it goes in coding.

Make sure you’re using the CB library the way the documentation describes.

Configuration on your Peripheral.

Because so much of what’s happening when your app connects to and communicates with your peripheral is happening within CoreBluetooth, it’s important to understand what configurations CoreBluetooth is expecting your peripheral to conform to.

BTLE enabled hardware devices have all selected a BTLE chip to use in their system. These chips allow a varying degree of configuration for how the chip will behave.

We found that CoreBluetooth’s expectations are pretty standard and often the defaul of how the chip ships. Still it’s good to be sure.

In terms of disconnection issues, it’s good to look at your peripherals ‘Connection Parameters’ and ensure they conform to CB guidelines. https://developer.apple.com/hardwaredrivers/BluetoothDesignGuidelines.pdf

Bluetooth as a Standard

In doing our research we opened up dialogue with some other BTLE related hardware start-ups. One using the same BTLE chip as us.. the other not. However, they both told us that they faced the same issues with connection stability.

The opinion of many engineers working in this space is that Bluetooth as a standard is not great. Lots of different chip manufacturers have implemented the protocol and the software that sits on top of the chip. Chances are there are some incompatibilities.

Solutions (Hacks)

Following optimization/confirmation of the CoreBluetooth API usage and our peripheral’s configurations, our connection issues did not go away. Several of these solutions came from the other BTLE hardware startup companies we opened dialogue with while troubleshooting this issue.

  1. Reconnection logic following an non user initated disconnect. (Literally, detect the disconnection and begin looking for the device again.)
  2. Perform an initial write to your peripheral upon establishing a connection to ensure that it is actually there.
  3. If possible, don’t disconnect via CoreBluetooth. Send a command to your peripheral to disconnect itself. (This was a recommendation from another company’s engineer.. we did not necessarily observe improved connection stability following this change… but made it non-the-less.)
  4. Reconnect to a known peripheral if possible.

This last one I’ll elaborate on.

Initial Scan/Connect Implementation

Involved the following methods:

- (void)scanForPeripheralsWithServices:(NSArray *)serviceUUIDs options:(NSDictionary *)options;

Upon having the peripheral returned in the following callback

- (void)centralManager:(CBCentralManager *)central didDiscoverPeripheral:(CBPeripheral *)peripheral advertisementData:(NSDictionary *)advertisementData RSSI:(NSNumber *)RSSI;

Then calling connect with that peripheral

- (void)connectPeripheral:(CBPeripheral *)peripheral options:(NSDictionary *)options;

Improved Scan/Connect Implementation

Our connection stability improved after moving to storing the peripheral’s UUID and then reconnecting to the known device using:

- (NSArray *)retrievePeripheralsWithIdentifiers:(NSArray *)identifiers;

And then connecting to the found device:

- (void)connectPeripheral:(CBPeripheral *)peripheral options:(NSDictionary *)options;

Lessons Learned

Understand your use case.

BTLE app implementations can involve:

  • Multiple peripherals at a time
  • A single peripheral connection at a time, but perhaps many different devices during usage a single user’s app.
  • Just one dedicated BTLE peripheral.

Our use case ended up being the last one.. meaning we could implement an approach of scan once and then reconnect to the same peripheral over and over.

Keep your reconnect logic simple.

This type of hacking can spiral into a myriad of timeouts and callbacks. Our final (and simplest) solution involved actually no timeouts.

When doing iOS app development, most unit test suites are fine to run on the simulator. I found myself in the situation on a current project of mine that the app I’m developing uses libraries where it can’t run on a simulator.

The problem was that on app launch, in the app delegate, there are references to these libraries and the app then crashes. No unit tests get run.

I didn’t want to start littering if else statements in my AppDelegate.

I found the easiest thing to do was add a whole new AppDelegate. I called mine SpecAppDelegate

#import "SpecsAppDelegate.h"

@implementation SpecsAppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
    self.window.backgroundColor = [UIColor whiteColor];
    [self.window makeKeyAndVisible];
    return YES;
}

- (void)applicationWillResignActive:(UIApplication *)application { }
- (void)applicationDidEnterBackground:(UIApplication *)application { }
- (void)applicationWillEnterForeground:(UIApplication *)application { }
- (void)applicationDidBecomeActive:(UIApplication *)application { }
- (void)applicationWillTerminate:(UIApplication *)application { }

@end

And then use this in your main.m when you’re launching the app for unit tests.

int main(int argc, char* argv[]) {
    int returnValue;
    
    @autoreleasepool {
        BOOL inTests = (NSClassFromString(@"SenTestCase") != nil
                        || NSClassFromString(@"XCTest") != nil);
        
        if (inTests) {
            returnValue = UIApplicationMain(argc, argv, nil, @"SpecsAppDelegate");
        }
        else {
            returnValue = UIApplicationMain(argc, argv, nil, @"AppDelegate");
        }
    }
    
    return returnValue;
}

Let me lay something out there. (Pun intended) Auto layout sucks like a hangover on release day.

I’ve always been a proponent of using tools to make my life easier. I recently discovered PureLayout. Let me say, it’s freaking awesome.

It’s essentially just a wrapper and a more friendly API around iOS’s auto layout API. In the last 12 months as I’ve been learning and incorporating auto layout into my UI development, I’ve sort of created my own auto layout wrapper API. But, it was something I just expanded on as needed and never really polished.

In comes PureLayout. I spent some time trying it out and I now think I’ll go ahead and scrap my own wrapper API and switch totally to this.

I thought I’d show the progression of the same code as it moved from using explicitly the iOS autolayout API with my wrapper API, then to PureLayout. It’s pretty sweet.

My wrapper API

- (void)setToolbarToStandardWidth {
    self.viewsDictionary = @{@"toolbar" : self};
    [self addWidthConstraint:self viewName:@"toolbar"];
}

- (void)addWidthConstraint:(UIView *)view viewName:(NSString *)viewName {
    [view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:[NSString stringWithFormat:@"H:[%@(%i)]", viewName, [UIScreen mainScreen].bounds.size.width]
                                                                 options:0
                                                                 metrics:nil
                                                                   views:self.viewsDictionary]];
}

PureLayout

[self.pinPlaceholderView autoSetDimension:ALDimensionWidth toSize:[UIScreen mainScreen].bounds.size.width];

What I love about it is you don’t have to keep that dictionary of views or even think about it. You don’t have to deal with the visual format syntax. And you just get much more readability. Love.

Issue:

1. You're device is running iOS 7 and so, any apps you deploy to it have to be deployed from Xcode 5.
2. You have a project that you'd like to run that has a target SDK of iOS 6. This project uses things like the camera or microphone that have to be tested on a real device?

Xcode 5 only ships with support for iOS 7 targets. Blah. But, you can add support for earlier targets if you can get your hands on those earlier SDKs.

I needed iOS 6 to run the PhotoPicker sample project on my device. I found that in my Xcode 4 installation at

/Applications/Xcode4.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS6.1.sdk

Copy that into the same directory for you Xcode 5 installation and you're off.

I had the opportunity to put a presentation together for the Brooklyn iOS meetup recently and decided to do one based on 'Lessons Learned' from a self-taught stand point. I am an iOS developer largely self-taught and many things I learned along the way, I wish I had known earlier!

I think the meet-up has a range of people from lots of different experience levels and I wanted to engage all of them to also talk about what they've learned that they wish they knew earlier.

It went in a really cool direction with a lot of discussion around the tools people use. A lot of a developers power is in the tools they use and it seems like the iOS space is exploding with new and awesome tools to help you write apps more efficiently.

Here's the video of the presentation.



Also, a lot of tools were mentioned by audience members in the discussion following the presentation.. I'll mention here some of the tools I discussed and those mentioned by others:

Mentioned in presentation:

PaintCode, CocoaPods, AFNetworking, HockeyApp, TestFlight, RestKit(verdict: avoid), and MagicalRecord.

Mentioned in discussion:

iExplorer, Reveal, ImageOptim, Crashlytics, Pop, and Envision.