At this weekend’s TreeHacks2017 at Stanford University, we made an iOS app that connects people with same flights and travel plans together. After integrating the Facebook Graph API and Amadeus Flight Autocomplete API in the first 12 hours, I was attracted to the “old topic” again – Bluetooth LE. By old, I mean we have been playing around with this wireless technology for several hackathons before, but just relied on some higher level frameworks (such as BluetoothKit for iOS Swift) to achieve practical functionalities. Why not directly going into the CoreBluetooth framework that directly interacts with the Bluetooth module, which offers more flexibility and generality? And hmm, making an offline chat room that let flying travellers to chat on the plane seems to be an awesome idea… It was 7 PM on Saturday night, 15 hours left; let’s go!

Time flew as we delved deep into Apple’s documentations… Although we finally got the stuff to work one day after the hackathon, it may be good to keep a note of what I learnt which might also help other developers entering the realm of Internet of Things (IoT) and wireless communication, or just finding some cool stuff to work on in a hackathon.

Basic Concepts

Let’s first take a look at what the technology behind CoreBluetooth really is. The newest Bluetooth 4.* protocol defines two standards: the Classic Bluetooth and the new Bluetooth Low Energy (Bluetooth LE or BLE). The BLE, which is used in our case, is a protocol massively used in IoT devices. It achieves extremely low power consumption by sacrificing a little bit of data transmission throughput. Bluetooth Low Energy vs. Classic Bluetooth has some intuitive comparisons.
Classic-BT-vs-BLE

Essentially BLE works behind the publish-and-subscribe model. Sounds unfamiliar? Just understand it as a client-server model with no dedicated session per connection: Consider device A and device B are transmitting data to each other. The Classic Bluetooth protocol needs to maintain an active connection between two devices at all times even if no one is transmitting at some time in the middle. This means the devices are doing redundant radio broadcasting which consumes power heavily. However, BLE cleverly solves this problem and I will explain in the next section.

Two key parties in BLE is central and peripheral: a peripheral device has data which can be thought as a server, and a central device wants data which can be thought as a client. Simple enough, isn’t it? Since BLE is designed for IoT applications, let’s take a heart rate sensor for example: it has data of your heart rate, so the sensor is a peripheral that feeds data to one or more centrals, which can be your smartphone.

Data in peripheral are organized in a tree-like structure: each peripheral may have one or more services, and each services includes one or more characteristics, where each characteristic carry binary data. To recognize them, each peripheral device has a built-in identifier, and each service or characteristic has a pre-defined UUID, which is chosen or generated during developing. There’re 16-bit standard UUID strings defined in BLE protocol for common functionalities (such as battery level and time), but we can also use 128-bit customized UUIDs which can be generated by uuidgen command in *nix system:

$ uuidgen
785E0659-3143-4804-89CA-8722122E5379

Below is what this data structure looks like when I made my iPad to be a BLE peripheral; it includes several pre-defined system services and a custom service I defined at last which has a string as binary data: “Hello Bluetooth LE!”

Peripheral - Yingbo's iPad
	Identifier: 216FA5C2-67CE-081D-B90B-D30CCD6C9A3A
	Service #1: 
		UUID: ...
		Name: Device Information 
		Characteristic #1:
			UUID: ...
			Name: Model Number String
			Value: ...
		Characteristic #2:
			UUID: ...
			Name Manufacturer Name String
			Value: ...
	Service #2:
		UUID: ...
		Name: Current Time
		Characteristic #1:
			Name: Current Time
			...
		Characteristic #2:
			Name: Local Time Information
			...
	Service #3:
		...
		Name: Battery
		Characteristic #1: 
			...
			Name: Battery Level
	Service #4:
		UUID: 9B1F32B2-95FA-4E5A-8D10-5F704AC73DAB
		Characteristic #1:
			UUID: 7E1DF8E3-AA0E-4F16-B9AB-43B28D73AF25
			Value: <48656c6c 6f20426c 7565746f 6f746820 4c4521> 

How it works

As I mentioned before, BLE runs on publish-and-subscribe model. So how do data actually get transferred? Essentially, peripheral advertises services it has, and central reads characteristics’ values from whatever services and characteristics that interest it. You could understand it as the peripheral “pulses” several times a second to let the central know that it’s there. The central sends a data request whenever it needs them, and the peripheral sends back data as a response. As you can see, nothing like a “connection tunnel” is established during the whole transmission process; radios of peripherals and centrals are turned on only intermittently, which is usually a tiny fraction of the total running time and therefore greatly saves energy. The peripheral only consumes power when it’s advertising its services, or receiving or responding to a central’s request.

Also, each characteristic has a set of permissions, including readable or writable. All characteristics usually have the “readable” permissions for central to read. But sometimes a central also needs to send commands to peripherals in IoT perspective, or “send data” to peripherals in data transmission perspective. This becomes possible when the “writable” permission is added to the peripheral’s service when we create it. For example, we might want to send a command to the air conditioner at home to turn it on and off. The central sends a request to the peripheral for updating a characteristic’s value; the peripheral then performs the update and sends back response for confirmation.

Hmm, seems that we now have a client-server architecture, where the data are stored on client-side and bi-directional data transfer can be achieved. So far so good. (While considering the application of data transfer specifically, this “asymmetrical” structure may seem wired as data are stored on one side and no tunnel connection is established. Don’t worry; BLE just works this way!) But another problem: do we have to read a characteristic very frequently if the central constantly needs the newest data (which wastes energy if the value isn’t changed)? It turns out the answer is no because of a handy feature known as subscription. Any central is able to subscribe to a characteristic if it has notifiable permission. All centrals will be notified of the new value whenever a characteristic they subscribed to has changed value.

Alright, almost done for the theories. I summarized the general programming workflow for BLE programming on iOS, following Apple’s Core Bluetooth Programming Guide. The first column is central-side iOS app and the second column is peripheral-side; read it from up to down. One thing to notice is the delegate patterns between the central and peripheral app: basically a delegate method is a callback function that got executed when an action is done. Green boxes are used in the graph to represent delegate functions and arrow represent their callback relationships. Check out my earlier post if you are not familiar with this pattern.
BLE-workflow-chart

Sample Code & intersting points

Check out my Github repo for codes in Swift 3 implementing the above architecture. Here are some intersting points worth noticing:

Advertising the whole time?

Peripherals don’t need to be advertising at the time when a central connects to it. Advertising is just a way for a central to know the existence of a peripheral and the primary service it offers, so that central can keep track of it. Particularly, all discovered peripherals are stored in the array in our code:

var discoveredPeripherals = [CBPeripheral]()

Go ahead experiment it by first advertising a few seconds and then stop on the peripheral-side iDevice. Make sure the other iDevice running in central mode stores the peripheral, and try connect after the peripheral stops advertising: all its services and characteristics can still be retrieved as normal.

Value of a Characteristic: cached or dynamic?

Consider the following piece of code on the peripheral-side iOS app: it creates a service and a characteristic with initial value of a string in binary data.

let myValue = "The quick brown fox jumps over the lazy dog."
let myValueData = myValue.data(using: String.Encoding.utf8)

var myCharacteristic: CBMutableCharacteristic?
var myService: CBMutableService?
let myServiceUUID = CBUUID.init(string: ...)
let myCharacteristicUUID = CBUUID.init(string: ...)

func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
	if (peripheral.state == .poweredOn) {
		// this line triggers an run-time exeception
		myCharacteristic = CBMutableCharacteristic.init(type: myCharacteristicUUID,
			properties: [CBCharacteristicProperties.read, CBCharacteristicProperties.write, CBCharacteristicProperties.notify],
			value: myValueData! as Data,
			permissions: [CBAttributePermissions.readable, CBAttributePermissions.writeable])
		
		myService = CBMutableService.init(type: myServiceUUID, primary: true)
		myService!.characteristics = [myCharacteristic!]
		
		myPeripheralManager!.add(myService!)
		myPeripheralManager!.startAdvertising([CBAdvertisementDataServiceUUIDsKey : [myService!.uuid]])
	}	
}

The run-time exception looks like:

BluetoothCenter[2603:922701] *** Assertion failure in -[CBPeripheralManager addService:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/CoreBluetooth/CoreBluetooth-352.1/CoreBluetooth/CBPeripheralManager.m:305
BluetoothCenter[2603:922701] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Characteristics with cached values must be read-only'
*** First throw call stack:
(...)
libc++abi.dylib: terminating with uncaught exception of type NSException

Apple’s documentation on the construtor of CBMutableCharacteristic pretty much explains everything:

If you specify a value for the characteristic, the value is cached and its properties and permissions are set to read and readable, respectively. Therefore, if you need the value of a characteristic to be writeable, or if you expect the value to change during the lifetime of the published service to which the characteristic belongs, you must specify the value to be nil. So doing ensures that the value is treated dynamically and requested by the peripheral manager whenever the peripheral manager receives a read or write request from a central. When the peripheral manager receives a read or write request from a central, it calls the peripheralManager(_:didReceiveRead:) or the peripheralManager(_:didReceiveWrite:) methods of its delegate object, respectively.

In fact, if you create a characteristic with “readable” permission only and a central requests to read its value, peripheralManager(_:didReceiveRead:) will not be called on the peripheral due to caching of the characteristic. Handling of a “writable” characteristic’s value should be put in peripheralManager(_:didReceiveRead:) instead.

func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
	if (peripheral.state == .poweredOn) {
		...
		// pay attention to "value" 
		myCharacteristic = CBMutableCharacteristic.init(type: myCharacteristicUUID, 
			properties: [CBCharacteristicProperties.read, CBCharacteristicProperties.write, CBCharacteristicProperties.notify],
			value: nil,
			permissions: [CBAttributePermissions.readable, CBAttributePermissions.writeable])
		...
	}	
}

func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveRead request: CBATTRequest) {
    request.value = myValueData!
    myPeripheralManager?.respond(to: request, withResult: CBATTError.success)
}

Maximum size of a characteristic’s value

A characteristic’s value cannot have size more than 20 or 28 bytes according to some documentations like this. Apple also specifies an approach of peripheral responding to a characteristic read by offset; see Performing Common Peripheral Role Tasks. Particularly I set up a long enough string as the advertising characteristic’s value and tested on an iPhone 5 with iOS 9.1 and an iPhone 6 with iOS 10.2.

let myValue = "The quick brown fox jumps over the lazy dog. Can the value of a characteristic has size larger than 20 bytes?"

First the iPhone 6 is set up as the peripheral and the iPhone 5 requests to read its characteristic, which behaves normally.

Characteristic data for <
	CBCharacteristic: 0x14da4c80, 
	UUID = 7E1DF8E3-AA0E-4F16-B9AB-43B28D73AF25, 
	properties = 0x2, 
	value = <54686520 71756963 6b206272 6f776e20 666f7820 6a756d70 73206f76 65722074 6865206c 617a7920 646f672e 2043616e 20746865 2076616c 7565206f 66206120 63686172 61637465 72697374 69632068 61732073 697a6520 6c617267 65722074 68616e20 32302062 79746573 3f>, 
	notifying = NO>: 
	The quick brown fox jumps over the lazy dog. Can the value of a characteristic has size larger than 20 bytes?

However, things get intersting when the two devices reverse their roles. Obviously the later part of the string got messed up after the iOS 9 device responds with the read request from the iOS 10 central. Taking a look at the log for peripheralManager(_:didReceiveRead:) on the peripheral, we notice that the characteristic’s value is sent over multiple response messages this time, each time with a different offset.

didReceiveRead: <CBATTRequest: ... 
	Offset = 0, 
	Value = (null)>
respond: <CBATTRequest: ... 
	Offset = 0, 
	Value = <54686520 71756963 6b206272 6f776e20 666f7820 6a756d70 73206f76 65722074 6865206c 617a7920 646f672e 2043616e 20746865 2076616c 7565206f 66206120 63686172 61637465 72697374 69632068 61732073 697a6520 6c617267 65722074 68616e20 32302062 79746573 3f>>

...

didReceiveRead: <CBATTRequest: ... 
	Offset = 456, 
	Value = (null)>
respond: <CBATTRequest: ... 
	Offset = 456, 
	Value = <54686520 71756963 6b206272 6f776e20 666f7820 6a756d70 73206f76 65722074 6865206c 617a7920 646f672e 2043616e 20746865 2076616c 7565206f 66206120 63686172 61637465 72697374 69632068 61732073 697a6520 6c617267 65722074 68616e20 32302062 79746573 3f>>

My guessing is that Apple supports larger message size for a characteristic’s read response in iOS 10. However, it’s always good practice to strictly follow the size limit defined in BLE protocol and construct the response message based on request’s offset.

Don’t confuse BLE with Classic Bluetooth

The key of designing a BLE application is to keep its working principle in mind: publish-and-subscribe model without dedicated session. Several concepts and terms can be confusing to beginners on BLE, especially if they have previous experiences on a data transmission protocol with dedicated tunnel, such as Classic Bluetooth.

  1. The connect(_ peripheral: CBPeripheral, options: [String : Any]? = nil) method in CBCentralManager does NOT set up a tunnel for future data transfer! (Sounds counter-intuitive though) As mentioned before, it just does necessary preparations for discovering services and characteristics of a peripheral.
  2. The concept of pairing does exist in BLE, but it’s not related to data transmission session in any way either unlike in Classic Bluetooth. Pairing of two devices is needed when extra security protection to a characteristic’s data is set up before: that is, only centrals that are previously paired with the peripheral have the permission to read its characteristic. See Best Practices for Setting Up Your Local Device as a Peripheral for details.

Application Design: an offline chat room

Okay, time to do some real work. I came up with mainly two architectures that might work for the chat room: more than two people who are within BLE’s range should be able to join and post messages. Also, ideally we can separate this large chat room into various topics; people join the sub-room with topics they are interested in.

Approach #1

A first natural thought would be setting up each participating device to be both a peripheral and a central: it advertises a service for its own chat messages while continuously receiving updates for other devices’ messages. This approach involves setting up both a CBPeripheralManager and a CBCentalManager in iOS particularly.

It turns out that performing two roles simultaneously on one device is indeed possible, as long as it does not advertise services (as peripheral role) and scan for peripherals (as central role) at the same time. But how could a device know how many other devices are participating, so that it can subscribe to all their characteristics to receive message updates? Hmm… We could probably define a “public service” which has a known UUID to every device, that is used for sharing “private characteristic” UUIDs of every new participating devices?

Approach #2

Instead of each device maintaining its own service, the program architecture could be simplified by setting up the first guy joining the chat to be “host”, which is the only peripheral device; it keeps a service of everyone’s message in the chat room. Anyone else joining the chat room later acts as a central device that subscribes to this service and updates its characteristic, so everyone will receive updates to new messages.

This architecture is much simpler to implement because the UUID for the only service and characteristic can be fixed, so the process of UUID sharing can be avoided. Each device scans for peripherals advertising this service for some time when the app is launched. Be a central and subscribes to this service if there’re non-empty results; otherwise be a peripheral and continuously advertising this service to others.

Approach #3

Perhaps we just don’t need to do so much work of program design, or even no need to use CoreBluetooth framework at all? Seems that Apple’s Multipeer Connectivity should be a starting point. We will definitely explore this option soon.

Future

An exciting world of IoT and wireless communication opens ahead of us. Stay tuned for update! And special thanks to my teammates at TreeHacks – Sara, Huyanh, Phillip and Amy – for supporting me playing around with “some sort of random technology” :-D

Reference

Introduction to Bluetooth LE
What is the difference between Ethernet and Serial communication?
Peripheral and central at the same time on iOS
Building a Chat App in Swift Using Multipeer Connectivity Framework