Bluetooth Low Energy has been built into iOS since iOS 5, almost 5 years ago. But how do you use it? Yoav Schwartz gives us a quick-fire overview of the basic concepts and shows us the basics of connecting to devices in iOS, passing down knowledge he has gained through fixing bugs and errors that brought tears to his eyes.
My name is Yoav Schwartz. I work for Donkey Republic, it’s a bike sharing system working in Copenhagen. In this post, I am going to talk about practical CoreBluetooth. I’m going to introduce you to Bluetooth Low Energy (BLE), because not everyone knows what it is. Then, I will introduce
CoreBluetooth, which is the Apple framework to handle low power Bluetooth. And then I will share some practical tips for development which I have gained through a lot of debugging, hair-pulling, and crying.
Bluetooth Low Energy (1:05)
First, what is BLE? It’s sort of like Bluetooth. We all know Bluetooth, we all use it in speakers and so on, but the difference is this is a very low powered protocol. Typically, one battery can last a month or even years depending on how it is used. That allows us to do things we normally couldn’t do with Bluetooth. The standard is called Bluetooth 4.0, it started with something called “Smart Bluetooth” and evolved to this one, there’s a 200 page manual you can read before you go to sleep, it’s a gripping read. BLE is very cheap in terms of power use and the protocol is not too complex.
Now, why BLE? What can we even use it for? The first and most common example is a heart rate monitor. Basically, it measures your heart rate and on the protocol it sends what your heart rate is and it’s used in running gear and stuff like that. Then we have sensors, you can hook up basically any sensor with BLE and then it would send you what that sensor is reading. Next, we have iBeacons, which can tell you “proximity” to a location, but I’ve written that in quotes because specifically on iPhone, you can’t see iBeacons as Bluetooth devices because Apple blocked that option, so you only work with the core location, because that’s what Apple does. But more in general it’s the Internet of Things, you can connect your Air Conditioning to a lower power bluteooth, you can connect your whatever, TV, and just as long as you make it work you could, it’s just a protocol you use to talk to a chip.
How does it work? (3:07)
We have a peripheral, which is what we call all the devices that work with Bluetooth. Each peripheral has services, it can have as many services as you want, and each one of these services has characteristics. Each one of these services has characteristics and you can basically treat the peripheral as a server, when you’re talking to a server there’s everything that is involved with the async communication, there’s sometimes it disconnects, sometimes it takes some time to arrive and sometimes it doesn’t arrive at all. But in general you have a service, then this service holds many characteristics, and each characteristic holds a type, value, properties and so on. You don’t have to know the entire thing to work with
CoreBluetooth, the most important thing is the reading, this is what we’re actually trying to get and manipulate and change, whatever you need to do, we want this reading and we want to know what we can do with it. That was a very brief introduction to BLE, and the reason it was a short introduction is because I’m sure there are thousands of talks online which explain BLE way better than me.
Core Bluetooth (4:48)
CoreBluetooth was introduced in iOS 5, which is pretty early. Apple worked pretty early in getting BLE into their devices, way before Android and the growth has been huge. There’s many, many people who use
CoreBluetooth in their apps. And it is just a wrapper, so these BLE protocols, they’re really complicated, I mean not really complicated but not something you’d want to work with on a daily basis. Just like many things, Apple wraps it for us in a very nice and tight package using terms that all of us stupid iOS developers can understand.
Get more development news like this
I’m going to explain the classes involved in talking to this
CoreBluetooth, because that’s what we really want to know. Our main actor, or our like chief manager, is the
CBCentralManager. We’ll initiate one of those, we’ll call here:
manager = CBCentralManager(delegate:self, queue:nil, options: nil)
We initialize by calling a new manager, and we can set a delegate, we have to set delegate because we can’t really use it without it. We set a queue on it, which is we initialize a queue if we want to, all the calls will be sent on that queue and if we send
nil, it will go to main, it will go to the main queue. Now, it really depends on what exactly it is that you want to do, if you actually need to use an extra queue, because it raises the level of difficulty but then again your users will love you more. If you need to talk with one device, feel free to use the main, but if you’re starting to go fancy, then just set up a queue and send to main before you use your other functions. Finally, options are not that interesting, there’s like one or two options, but the main used one is that when you initialize it, if your user doesn’t have their Bluetooth turned on, it will ask them but all the users just press OK and it doesn’t actually turn it on so I just never use it.
This object has one main call:
func centralManagerDidUpdateState(central: CBCentralManager!)
You have to receive didUpdateState didUpdateState, and the first tip I’m going to give you is that you can’t do anything with the manager, the manager is useless until you’ve actually gotten this didUpdateState to
.PoweredOn. And the rest of the states are really not that interesting, there’s not like it could help your user besides telling him to turn on the Bluetooth.
Scanning for devices (9:38)
Now that we have our manager up and running we want to start looking at what devices are around us. We take that manager we created and got the
.PoweredOn, and we call
manager.scanForPeripheralsWithServices([CBUUID], options: nil)
With the services, it’s just an array of
CBUUIDs that you basically tell the device, “I only want to hear about this kind of device with this kind of UID”. It’s a common theme in Bluetooth, if you send
nil it will just give you everything. It’s better for performance to give it an array, but if you don’t know, it’s OK to just give it
nil, no-one will die. Right, we start a scan physically and we also have to stop it. Otherwise the device would just keep on scanning and keep on spending your user’s battery until you stop it, so when you’ve got what you’re looking for, or when you don’t need to know about peripherals anymore, just stop scanning:
When you discover a peripheral, when there’s a peripheral around you, and there usually will be, then it would call
didDiscoverPeripheral on the queue we decided previously, giving us a
advertisementData which is just a dictionary of something that the chip designer decided is going to be sent every time. RSSI, which is a relative signal strength, it just comes in decibels and it can tell you about how strong the signal is:
func centralManager(central: CBCentralManager!, didDiscoverPeripheral peripheral: CBPeripheral!, advertisementData: [NSObject : AnyObject]!, RSSI: NSNumber!)
The second big tip is keep a
strong reference for the discovered peripherals because otherwise if you don’t keep a
strong reference the system assumes that you don’t care about that one and it will just throw it away, so you can’t actually start using it without keeping a
strong reference to it. I mean, the system does keep it somewhere but we don’t really have access to it.
Connecting to a peripheral (11:35)
Now that we’ve found the peripheral we’re interested in, it’s kind of like going to a party, we see a girl we like, now we want to connect. So we call
connectPeripheral which is like, “Buy drink.” And then we offer the peripheral to connect with it and it can say yes, it can say no, hopefully your iPhone is really pretty, so it’s going to say yes.
manager.connectPeripheral(peri, options: nil)
Then, we call the manager, he’s the one in charge of the connections and you call
connectPeripheral on the peripheral you want, and again, options
nil. If you’re really super interested in the option, you can go on and check it out, but normally you can be OK with
nil. And then when we’re done with the peripheral, you know, in the morning, we can
//called to cancel and/or disconnect manager.cancelPeripheralConnection(peripheral)
When we call connect or cancel, when it’s done we would get
didConnect and unsurprisingly
didDisconnect from peripheral:
//didConnet func centralManager(central: CBCentralManager!, didConnectPeripheral peripheral: CBPeripheral!) //didDisconnect func centralManager(central: CBCentralManager!, didDisconnectPeripheral peripheral: CBPeripheral!, error: NSError!)
Now, the two important tips, and again those tips I know are pretty far out for people hearing about Bluetooth for the first time but I will upload the keynote and I’m sure that if you do work on it, then you’ll go back to the tip and say, “Ah,” cause that’s what I did. And so tip number three is, even though the protocol itself defines a timeout, Apple don’t really care what the protocol says. So, iOS will just keep trying to connect and connect and connect and they would never stop, in fact, until you call
cancelPeripheral. Sometimes the connection can take very long, so you probably should time it, like use a timer to make sure that you’re not taking too long. And if you didn’t get
didConnect, then just cancel and tell your user that you couldn’t connect to it. Tip number four is, again if you don’t keep a
strong reference to the peripheral, even if you’ve connected to it, if you lose that pointer to it, the OS would just dump the connection. Because it assumes you don’t want it and maintaining a Bluetooth connection is battery heavy and you know what Apple thinks of battery.
Make the peripheral useful (13:51)
Now we have connected to the peripheral, but let’s do something with it. I’ll take you back to the start of the post, we talked about the services and the characteristics and the values which is, in the end, what we need. We start our tree with calling, now that we have the peripheral and it’s connected, we call
discoverServices, we would get
didDiscoverServices and then when those two are done, we can call our
peripheral.services, which is basically an array of all the services we have:
peripheral.discoverServices([CBUUID]) func peripheral(peripheral: CBPeripheral!, didDiscoverServices error: NSError!) peripheral.services
Now, it’s kind of hard to find this out, but this delegate is actually called on the same thread we decided on initially, so even though it’s actually the peripheral and not the manager that we call
discoverServices on, it kind of keeps it in memory, of which thread it’s talking to. So your entire Bluetooth, actually, will be on this thread. Remember to call main, because that sucks if you don’t.
We have the services but still we don’t have something we can work with. We have to take all the services we found and we call
peripheral.discoverCharacteristics and then it will take all the services we got and it will call
discoverCharacteristics on them. It’s like a regular delegate pattern. We have
didDiscoverCharacteristicsForService and then once we have those characteristics we can call
readValue, which will read the most current value. Or we can
setNotifyValue, so that’s kind of like telling the server or the peripheral, “Tell us when something changes.”
peripheral.discoverCharacteristics(nil, forService: (service as CBService)) func peripheral(peripheral: CBPeripheral!, didDiscoverCharacteristicsForService service: CBService!, error: NSError!) peripheral.readValueForCharacteristic(characteristic) peripheral.setNotifyValue(true, forCharacteristic: characteristic) func peripheral(peripheral: CBPeripheral!, didUpdateValueForCharacteristic characteristic: CBCharacteristic!, error: NSError!)
The tip here is that, unlike Android, Apple actually didn’t distinguish between a read and a notification, so you can’t actually know if you read information or if the device sent to you, you can’t know specifically if it meant a change or did do a read or when did it happen. Normally it’s more useful to keep a state than to actually use a reactive way of handling the events.
Writing to a peripheral (16:19)
We have the peripheral, we read it, we know what it is, we have the entire peripheral. Now we can write, basically,
NSData. Just basically binary you write to it. And the truth is that you kind of have to know what device you’re talking to to know what it will accept and what to expect from it. Most devices will come with some kind of spec which would let you know, but you can also kind of look at the messages to see what it’s actually expecting. And then this characteristic has some properties which tell you if you can read it, if you can write it, if it can do notifications etc. Basically all the abilities of this characteristic. And
isNotifying, which will tell you, “Are you signed up?”
peripheral.writeValue(data: NSData!, forCharacteristic: CBCharacteristic!, type: CBCharacteristicWriteType) characteristic.properties - OptionSet type characteristic.isNotifying func peripheral(peripheral: CBPeripheral!, didWriteValueForCharacteristic characteristic: CBCharacteristic!, error: NSError!)
When you write, like everything in this protocol, you get a
didWriteValueForCharacteristics and then you know it worked and you can show success to your user, and you know the value updated.
Practical Tips for CoreBluetooth Development (17:32)
I know this a pretty quick brief of what BLE is and what Bluetooth is, but this is the meat, this is the part I was kind of waiting for it to get to, so that I can share some of my experiences, like what I learned from working with it. Because it’s meant for a very specific case, the way Apple made it, and if you try to kind of go out of it, then you find quite a lot of problems, for example, the really heavy reliance on the delegate pattern.
CBPeripheral? If only it were that simple (18:15)
If you think about it this way, I have this peripheral, but the fact that it’s a peripheral doesn’t really tell me anything. I probably want to talk to a lock or an air conditioner or a heart rate monitor. The first thing I wanted to do as a developer is that I personally knew that it’s a more specific thing, it is that thing but it’s more specific so it sounds like a subclassing pattern. That’s the first thing you want to do when you know you’re only going to be working with that device, but unfortunately Apple made us a sad panda. Because
CBPeripheral actually inherits from none and conforms to none. That means it’s not actually an
NSObject, and although you can subclass it and you’ll see some of the things working, when they don’t work, you have no idea why. I would really advise against subclassing it. But what can we do?
Composition to the rescue. What I recommend you do is that you take the
CBPeripheral and you put it in an object that manages it, and you initialize that object with a
CBPeripheral and if you’re writing Swift or you make it a read-only, like a
let, and then you never touch it again. It encapsulates your peripheral and then you put all the methods for using this peripheral on that encapsulator and I will return to this encapsulator with the rest of the tips, but I really recommend it. Use it to interact with the peripheral, to keep a reference to it so the OS doesn’t throw the connection, and the most important part is it acts as its delegate. Because otherwise it’s very tough to manage all of your peripheral in the same place, it causes a lot of
Connecting to the peripheral and the
Now we want to connect to the peripheral, and be the
CBPeripheralDelegate. But again Apple decided to troll us, because while you do everything with the peripheral and the
CBPeripheralDelegate, and you call
discoverCharacteristics, right? Whatever you do, you call on the peripheral, besides one thing, connect to it. So now you’re kind of in the middle where you want to be your own. When you want the peripheral to kind of control the self and encapsulate, but the manager has to be in charge of disconnection and connection and everything, and it’s very hard to keep everyone informed, to keep one source of truth in the app.
What I do, and again it’s sort of a leap of faith, because like you saw before, it doesn’t conform to
NSObject. But I observe the state of the peripheral, and the state of the peripheral can go between disconnected, connected and connecting. It will always tell you what your situation is, so you can kind of call connect on the
CBPeripheral and then not follow that, in fact, all you have to do is, in your wrapper, subscribe to the state and then when you get a “connected” or a “disconnected” then you know what’s going on.
Here is a warning: like I said, it doesn’t inherit from
NSObject so KVO compliance is not guaranteed. However, I’ve seen it in a lot of code, a lot of wrappers over a
CoreBluetooth and it works, so I’m willing to risk my app on it, but yeah, you can each decide on your own, just a friendly warning.
Determining proximity (21:58)
The last one which is probably the biggest one because it’s kind of hard to find proper documentation on it. Now, Apple with their iBeacons, they determine proximity, they tell us how close you are to the device, if anyone has ever worked on it. Unfortunately, they don’t give us a neat way to do it with our own devices and many times you would like to know what the closest device is, maybe how close is it even though it’s really hard to know, and especially if it’s in range or not. Because sometimes you would get one advertisement from the device but then never again, so when we try to connect to it, we are very unlikely to succeed. What we do is that in that object I described that wraps the peripheral, we basically keep a stack of the timestamp and signal strengths of every one of those advertisements, of every one of those packets that we get in the
discoverPeripheral and we keep it in a stack. So you can decide what your stack size is, I found out that 20 is a pretty comfortable number.
If anyone has used
CoreLocation, it’s sort of like the stack of locations that it saves, so it saves a stack of locations, it saves a timestamp and the location coordinates. And then to determine the proximity, basically the bigger the RSSI, the close the device is. The closest device to you is almost always guaranteed to be the one with the highest RSSI, the RSSI is the signal strength. Now, if it’s in range or not is a bit more complicated because it’s also a very soft term. Like, what is in range? Is 50 meters in range, is 100 meters in range? Just, what is? So what I use is basically a low pass filter, what I do is I take all the stack, and that’s why we collected the stack, and I run an average RSSI on it. However, if I just run an average then it could be that I got something 20 seconds ago and it would still count, so what I do is, I do actually a weighted average.
I basically divide each one of those RSSIs by how long ago it was, this creates that the most recent ones have the highest weight in the average, so it would be like let’s say you’ve got a 20 and then a 10, then it will be much closer to 10 than a 20. But there are many possible implementations, basically if you’re reading on determining WiFi signal, you’re reading on that too, because signal and antennas and waves, they all work the same. And the one little gotcha here, and I’m almost at the end, the one little gotcha here is, the connected peripherals, the one you’re already connected to, they stop sending you messages, or they do send you a message but they don’t advertise, it’s called. You actually have to call
peripheral.readRSSI manually every time you want to get a new read about how strong their signal is.
Now What? (24:57)
You read this post and you’re interested, but obviously these short paragraphs are not going to make you an expert. Apple’s CoreBluetooth Programming Guide is probably the first thing you should read because it’s not that long and it’s very useful. Then there were two talks on
CoreBluetooth from WWDC 2012 (basic and advanced) and one from 2013, but not a lot has changed, don’t worry.
And there’s a video from Altconf 2015, actually hosted on Realm’s site. It’s by a really nice guy, Jon Shier, who speaks about
CoreBluetooth and does a really good job with all the background and stuff.
Something Extra (25:42)
A little something extra from me, so I’ve talked a bit about
CoreBluetooth, but what happens now? You go home, you don’t actually have a
CoreBluetooth device, you don’t have to have a heart rate monitor. But the truth is that you do because in my talk I talked about how you can be the consumer that talks to this peripheral, but in real life we have almost an identical set of classes that make you not the manager but the peripheral. If you have two iOS devices, you can basically become a firmware developer yourself. One of your devices will be the peripheral and one of your devices would be the consumer, the manager. You can basically design whatever chip you want, raise the temperature, lower the temperature, whatever you want, you can mock it. And then when you’re done and you have your perfect product and after you’ve shown it off, you’ve got your money and you’ve got Kickstarter and whatever, go to a chip designer, or a firmware developer, and you give them a full spec and you say, “Do this, don’t think, just do this.” And that’s it, you’re done. Don’t forget, do this.
I am Yoav Schwartz. If you like this post you can follow me on Twitter, I’ll post the talk, if you didn’t like this talk, also follow me on Twitter because I have really low followers and it’s not that nice. Thanks to NSCocoaHeads Copenhagen and that is it. Thank you.
Q:Does everything work on the Simulator?
Apple actually supported the Simulator until 2012 I think, or 2013, but then in the second WWDC talk, they talk about why they stopped actually supporting Simulator. Because
CoreBluetooth was getting too advanced and the difference between how it works on Mac and how it works on iOS became too split apart for them to maintain it. So in language we can all understand, they were too lazy to do it, so you need a device to kind of play with it, that’s also closer to the truth.
Q:Do you know what the data limits are for sending and reading? Because I think two years ago they didn’t specify it that well, it was hard to find out and someone said something, and some others said something else and yeah.
I think you’re referring to the first ever WWDC talk, when after that, then they introduced it, people asked them how fast can it go and they didn’t really know how to answer, actually you’re right. On the second one they show kind of a graph and how hey improved it, so I think it started from 50 kilobytes per second and now they made some crazy tweaks and whatever and they’re really excited about it and it’s 100. But I’m really not sure about the numbers, but the talk has it.
Q:Is that also for the low energy?
Yeah, that’s the low energy I’m speaking about.
Q:And how much can you send in one of those packets?
I don’t know exactly how much because it kind of depends on the implementation, I don’t know if it’s actually written in the protocol. I’m not sure, I think it’s up to your devices but yeah, I can check it for you and come back or you can see the second WWDC talk and they have like a nice graph, I don’t remember their specific numbers. I don’t think they ever specified how much actually, well, they might have said something. I think what was mostly talked about is if you can do over the air updates, which was the really interesting part. And apparently programmers made it, they made over the air updates and they made images transfer. More than that, I know it’s possible and I know it’s not that hard, but more than that I’m not sure, you have to be smart about it.
Q:What about latency?
Again it depends on how you set up the chip but normally it’s something around 30 packets per second and packets just come and go all the time, because you’re in active connection with that. It’s like having an open TCP channel. So the latency is actually really small. Like, barely noticeable, I’m almost sad it’s async because it makes everything harder. Mostly it’s unnoticeable. Besides connection, that can take varying times. Connection is sort of like, it’s super, it’s really hard to tell because it has to do with antennas and it has to do with black magic and the air. It’s really hard to explain why sometimes it’ll take one second to connect and sometimes it’s 10. In general, talking to the device it’s fine, and in connecting, put a 10 second timer on it and you’ll be golden.
About the content
This content has been published here with the express permission of the author.