Bluetooth LE for Mobile - 2. iOS Peripheral

The story so far:

  1. An Overview
  2. iOS Peripheral
  3. iOS Central
  4. Android Peripheral

Core Bluetooth and iOS

In the last post I described Bluetooth Low Energy (BLE) at a high level and explained roughly how it works on iOS and Android phones. Now I want to talk about the iOS API; in particular how to be a peripheral. I’m not going to go into heaps of detail – that’s what the documentation is for – but in this post I’ll introduce the key classes, describe what coding you’ll need to do, and highlight quirks that are interesting and/or dangerous.

The framework for Bluetooth LE communication on iOS and macOS is called Core Bluetooth. Despite the broad name it is almost entirely for GATT communication, which is all about the services and characteristics mentioned previously.

Prerequisites

Your device must have Bluetooth turned on. Obviously. If you want your app to be able to act as a Central or Peripheral when it is not in the foreground you must enable the bluetooth-central or bluetooth-peripheral Background Modes in Xcode.

Being a Peripheral

To turn your iPhone or iPad or Mac into a Bluetooth LE peripheral you need an instance of CBPeripheralManager. You will need to provide it a delegate of type CBPeripheralManagerDelegate. Implement this in your class and pass it in when you initialise the CBPeripheralManager.

The required steps are described pretty well in the Apple documentation Performing Common Peripheral Role Tasks (albeit in Objective-C) so I won’t bore you with code snippets. Here’s a diagram though.

First you need to allocate UUIDs for your services and characteristics. To create a CBMutableCharacteristic you need to specify its UUID and its permissions such as whether it is readable or writable. Then you create a CBMutableService and assign an array of characteristics to it. If you’re unsure you probably want to create a primary service.

Once you’ve built up a CBMutableService, add it to the CBPeripheralManager with add(). Finally, when all your services are added, call startAdvertising() on that manager, providing the device name and the array of advertised service UUIDs. Your delegate will be called with peripheralManagerDidStartAdvertising(). If the error parameter is nil, congratulations, your peripheral is now live.

Tip: It’s all async

When using CBPeripheralManager or CBCentralManager look closely at their respective delegate methods, implement all of them, and understand when they are called and why.

Error handling is critical because in BLE errors happen all the damn time. It may be due to lack of permissions, Bluetooth being switched off, or congestion on 2.4 GHz. It is way less reliable than just about any other API. It’s not Apple’s fault; it’s just the nature of things and if an app doesn’t pay attention and manage its state properly it will have a bad time.

Virtually every action that you can take with the BLE API takes a certain amount of time to complete and it will tell the delegate when it succeeds or fails. It’s important to wait for these notifications before doing the next thing. For example, disconnecting from a device takes time. If you attempt to reconnect before receiving didDisconnectPeripheral then it will not end well.

It is somewhat unfortunate that iOS wound up with a “massive delegate” design for this API. There’s an argument to be made for farming off particular tasks and responsibilities to multiple smaller classes. You’re forced into writing a big stateful class that can handle an arbitrary number of characteristics for an arbitrary number of connecting peripherals. Recognise that this is going to happen and do your best to move complexity out to helper classes.

Handling read and write requests

Surprisingly, if you are an iOS peripheral the operating system will not tell you when a central connects or disconnects. It just isn’t part of the API. So your app will be advertising merrily away and suddenly it will be interrupted by a read or write request. (It could be also be a subscription event – more about those shortly.)

There are two delegate methods for responding to read and write requests. For a read you must provide a Data (NSData) containing the bytes to return. In a write request are provided with a CBAttRequest that contains the bytes that were written by the central.

In both cases the delegate is given two pieces of information – a CBCentral, who has a UUID identifier that tracks a particular device for the duration of its connection, and the CBCharacteristic that was targeted, which contains the UUID you assigned to it earlier on.

There is no rule that you have to give every central the same value for a particular characteristic. There is no rule that you have to provide the same value every time a central reads a given characteristic. Let your imagination run wild – but realise that if you rely on a series of reads or writes, the longer your process goes, the lower the probability that it will fully succeed.

However there are two important things that the iOS peripheral does not know:

  • If a central reads a value and you provide it, you don’t know for sure if they ever got it.
  • If a central writes a value and you use it, you don’t know for sure they got the acknowledgement that the write was successful.

This is not especially limiting but it’s something to keep in mind if you want your app to be fault tolerant.

The trouble with subscriptions

Have you ever written an app that gummed up iOS so badly that the entire device needs a reboot to start working again? I’ve done it only once and it was using BLE subscriptions.

A connected central can choose to subscribe to a characteristic. Your app will receive delegate calls when they subscribe and unsubscribe. When the characteristic notionally “changes value”, or perhaps you just want to send a bunch of chunks of data in a row, you can invoke updateValue() to tell iOS that it should transmit a new value to the relevant subscribers.

But sometimes updateValue() returns false, indicating failure. Have a look at this crazy method.

You have to wait for this mysterious queue to empty out a bit, at which point you receive this call on your delegate. You can then do more updateValue()s until one of them returns false. Then you have to wait again.

Okay so this is kind of clunky and state-heavy but you can work with it to get the data through – unless the connection to the device fails abruptly. In that case iOS sometimes gets stuck trying to communicate with devices that don’t exist, the queue never empties, and it all goes to hell in a handbasket.

I should mention that I did most of this testing on iOS 9 which is now a little dated. All the same I would recommend avoiding the subscription API unless you specifically need sporadic realtime updates from peripheral to central – i.e., exactly what it was designed for.

For everything else, such as sending larger chunks of data, you are far better off to have the central do just one read or write at a time, wait for the success result, then do the next one. This way you never pile up on any queues and failures can be handled far more gracefully.

Big Data with iOS BLE

Under normal circumstances BLE characteristic values are limited to 20 bytes in length. When the central and peripheral are both running iOS the rules can be bent a little.

CBCentral has a maximumValueLength parameter which indicates how many bytes you’re allowed to send it at once. I have seen iPad centrals reporting values of around 160 bytes. I don’t have measurements but this may speed things up for you if you’re trying to send a lot of data via characteristic reads.

Next steps

In the next post I will describe being a BLE central in iOS. Originally I was going to cover all of iOS in this post but it was getting way too long.

Part 3: iOS Central