Protobufs 💕 BLE

Bluetooth Low Energy + Protocol Buffers

a.k.a. BLEProtobufs


Protocol buffers (protobufs) are the hot new data transport scheme created and open-sourced by Google. At it’s core, it is a way to describe well-structured model objects that can be compiled into native code for a wide variety of programming languages. The primary implementation provided by Google supports Objective-C, but not Swift. However, thanks to extensible capabilities, Apple has been able to release a Swift plug-in that enables the protocol declarations to be compiled into Swift 3 code.

The companion to this is a framework (distributed along with the plug-in) that handles the transformation of the model objects to and from JSON or a compressed binary format. It is this later capability that we are interested for the purposes of communicating with Bluetooth Low Energy (BLE) devices.

The primary selling point of protobufs is their ability to describe the data contract between devices running different programming languages, such as an iOS app and a .Net API server. There are dozens of excellent blog posts scattered about the web on protobufs, so that is all I will say about them here.

Here is the protobuf declaration for the message I will be sending between devices via BLE:

[gist file=”Packet.proto”]

A Quick BLE Primer

There are two primary actors in a BLE network: peripherals and centrals. Peripherals are devices which exist to provide data; they advertise their presence for all nearby devices to see. When connected to, they deliver periodic data updates (usually on the order of 1-2 times per second or less). The second type of device is known as a “central”, it can connect to multiple peripherals in order to read data from and write data to them.

A peripheral’s data is arranged into semantically-related groups called “services”. Within each service exists one or more data points, known as characteristics. Centrals can subscribe to the peripheral’s characteristics and will be notified when the value changes. The BLE standard favors brevity and low power consumption, so the default data payload of a characteristic is only 20 bytes (not kilobytes).

Data from a characteristic is received as just that, a plain Data object containing the bytes of the value. Thus, it is often incumbent upon the iOS developer to parse this data into native types like Int, Float, String, etc. This process is complex and error-prone, as working with individual bytes is not a common use case for Swift.

Enter Protobufs

As I mentioned above, protocol buffers can encode themselves in a compressed binary format. This makes them ideal for data transport over BLE where space is at a premium. In the example project I link to below, I am transmitting a timestamp in the form of an NSTimeInterval (double) cast to a float and three Int32 values representing the spacial orientation of the host device. I converted the rotational units from floating-point radians to integer- based degrees because integers compress much better than floating-point numbers in protobufs. After I set the properties the model object, I request its Data representation, which I save as the value of the characteristic. The data payload ranges from 5 to 12 bytes, based largely on the magnitude of the orientation angles (larger magnitude angles compress less). This is well below the 20 byte goal size.

In action:
[gist file=”ProtobufEncoding.swift”]

On the central (receiving) end, the app is notified via a delegate callback whenever the subscribed characteristic’s value changes. I take the Data value from the characteristic argument and pass it to the initializer of the protobuf-backed model object. Voila! Instant model object with populated properties that I can do with what I please.

In action:
[gist file=”ProtobufDecoding.swift”]

I have a pair of example projects available. The sending app is designed to be run on an iOS device and the receiving app is a simple OS X command line app built using Swift Package Manager (because frameworks + Swift CLI apps = hell). I’ve written the core of both apps using only Foundation and CoreBluetooth, so the sending and receiving roles should be easy to swap between different platforms.

Peripheral (sender) app

Central (receiver) app