CoreBluetooth in practice
Translation of the article Practical CoreBluetooth for Peripherals
A couple of years ago, when I first encountered Bluetooth with a working draft, I found this article, which greatly helped to understand how it works, to find the “starting point”. I hope that it will be useful for beginners.
About the author: Yoav Schwartz is a leading iOS developer in Donkey Republic, Copenhagen's bikering system, seeking to change attitudes to bike transport. Next, it will be on behalf of the author.
In this article I will talk about the practical tricks to work with CoreBluetooth. First, about Bluetooth Low Energy (BLE), because not everyone is familiar with this technology, then about CoreBluetooth, the Apple framework, which allows us to interact with BLE devices. I will also tell you about some techniques in the development, which I found out myself while I was debugging, weeping and tearing hair on my head.
Bluetooth Low Energy
For a start, what is BLE? It's kind of like Bluetooth, which we all use in speakers, headsets, etc., but there is a difference - this protocol consumes very little power. Usually, a single battery charge can be enough for months or even years for a device that works with BLE (depending on how this device is used, of course). This allows us to do things previously unavailable for “normal” Bluetooth. This standard is called Bluetooth 4.0, it all started with a technology called Smart Bluetooth, which later developed into BLE. There is a 200-page manual , you can read before bedtime, an exciting reading.
BLE is very economical in terms of energy consumption, and the protocol itself is not very complicated. So why ble? How can we use it? The first and most common example is the heart rate sensor. Usually this device measures and transmits your heart rate to the protocol. There are all sorts of sensors to which you can connect via BLE and read the data that they collect. Finally, there are iBeacons that can tell you “proximity” to a place. In quotes, because Apple’s iphone is blocking the ability to detect iBeacons as ordinary Bluetooth devices, so we have to work with CoreLocation. In general, this is the Internet of Things: you can connect to a TV or air conditioner, and communicate with it using this protocol.
How it works?
We have a peripheral - so-called devices that use the Bluetooth protocol. Each periferal has services, there can be as many of them, and each of them has characteristics. You can consider the periferal as a server. With all the consequences: it sometimes turns off, sometimes it takes time to transfer data, and sometimes this data does not come at all.
In general, we have a service with a variety of characteristics, each of which contains some value, type, and so on. To work with CoreBluetooth, you don’t need to know everything, the most important thing is to read data. This is what we are trying to get, change or use for our own purposes. We need this data and knowledge of what we can do with it.
Here is a brief introduction to BLE because there are thousands of resources that will explain the technical features better than me.
Core Bluetooth was introduced by Apple a long time ago, in iOS 5. Apple began work on introducing BLE into its devices much earlier than Android and the growing popularity of technology. Many developers use this framework in their applications, by and large - this is just a wrapper, since the BLE protocols themselves are quite complex. Not that much, but believe me, this is not something that I would like to work with every day. Just like many other things, Apple wrapped it up in a beautiful and convenient package, allowing us to use terms that all of us stupid developers can understand.
Now it is the turn to tell what you really need to know - about the classes involved in communication with the framework. Our main actor, CBCentralManager, will create it:
manager = CBCentralManager(delegate:self, queue:nil, options: nil)
Above, we created a new manager, specifying his delegate, otherwise we will not be able to use it. We also indicate the queue, in our case nil, which means that all communication with the manager will be carried out on the main line.
You need to understand exactly what you are going to do - using a separate queue will complicate the application, but, of course, users will love you more. If you plan to communicate with only one device, you can not bother and use the main queue. If you still want to experiment, then create a queue, specify it in the constructor and do not forget to return to the main one, before using the results obtained elsewhere.
Options There is nothing particularly interesting here, perhaps, the main thing is when you create a manager and the user has bluetooth turned off - the application will tell him about it, but almost everyone clicks “OK” (which actually doesn’t include bluetooth), which is why do not use.
The first thing after creating a manager is to call its delegate:
func centralManagerDidUpdateState(_ central: CBCentralManager)
So we will get a response from the hardware - whether the user has bluetoooth on or not.
First advice: the manager is useless until we get an answer that bluetоoth is turned on, his state is .PoweredOn. The remaining states can be used except to ask the user to turn on bluetooth.
Now that our manager is working properly, we can look at
what is around us (after receiving the state .PoweredOn, we call the scanForPeripheralsWithServices function :)
manager.scanForPeripheralsWithServices([CBUUID], options: nil)
As for services, this is an CBUUID array (a class representing 128-bit universal unique identifiers of attributes used by Bluetooth Low Energy approx. Lane), which we use as a filter to find devices only with this UID, it is common practice in CoreBluetooth .
If you pass nil as an argument, we can see all the devices around. For performance, of course, it is better to specify an array of the parameters we need, but in the case when you don’t know them, nothing terrible will happen, if you pass nil, no one will die.
Since we started the search for devices, we should stop it. Otherwise, the search will continue and put the user's battery until we stop it. As soon as we find the right device, or the need for searching disappears, we’ll stop:
Every time when a new device is detected, the didDiscoverPeripheral function will be called on the queue that we specified during its initialization. The function sends us the device found (peripheral), information about it (advertisementData is something that the chip developers decided to show every time) and the relative level of the RSSI signal in decibels.
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber)
Second tip: always keep a strong link to the desired periferal found. If this is not done, the system will decide that we do not need the device found and discard it. She will remember him, but we will no longer have access to him. Otherwise we will not be able to work with the device.
Connect to device
We found the device we are interested in - it's like coming to a party and seeing a pretty girl. We want to connect, call the connectPeripheral function - we offer “buy a drink”. So we try to connect to the right device (peripheral), and it can tell us “yes” or “no”, but our iPhone is really good, so we will hear a positive response.
manager.connectPeripheral(peripheral, options: nil)
Here we turned to the manager who is responsible for the connections, telling him which device we are connecting to, and again we give nil as options (if you are really interested in learning about the options, read the documentation, but you can usually do without them). When you finish working with the device, you can disconnect from it, well, you know, in the morning, cancelPeripheralConnection:
//called to cancel and/or disconnect manager.cancelPeripheralConnection(peripheral)
After we connect or disconnect the connection, the delegate will tell us about it:
//didConnect func centralManager(central: CBCentralManager!, didConnectPeripheral peripheral: CBPeripheral!) //didDisconnect func centralManager(central: CBCentralManager!, didDisconnectPeripheral peripheral: CBPeripheral!, error: NSError!)
Now, two more important tips. The Bluetooth protocol implies a connection timeout, but Apple does not care. iOS will try to connect again and again and will not stop until you call cancelPeripheralConnection. This process may take too long, so it is necessary to limit it in time, and if, in the end, we do not receive messages about successful connection (didConnectPeripheral) - you need to inform the user that something has gone wrong.
If you do not keep a strong link to the peripheral, iOS will simply drop the connection. From her point of view, this will mean that you don’t need it, and supporting it is quite an energy-intensive task for a battery, and we know how Apple relates to energy consumption.
Making the device useful
And so, we connected to the device, let's do something with it. Earlier, I mentioned services and features, the values they contain, that's what we need. Now we have a device, it is connected and we can get its services by calling peripheral.discoverServices.
peripheral.discoverServices(nil) func peripheral(peripheral: CBPeripheral!, didDiscoverServices error: NSError!) peripheral.services
Now it will sound a bit confusing, but the delegate is called on the thread that we defined when creating the manager, despite the fact that this is a delegate of the periferal. That is, the system remembers with which stream it works, and all of our Bluetooth communication happens on this stream. It is important not to forget to return to the main, if you did not use it.
We received services, but we still have nothing to work with. Next, you need to call peripheral.discoverCharacteristics, the delegate will give us all the available characteristics for the received services in didDiscoverCharacteristicsForService. Now we can read the values that
are contained there (readValueForCharacteristic) or ask to let us know as soon as something changes there - setNotifyValue.
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!)
Unlike Android, Apple does not distinguish between reading and notification. That is, we do not know what is happening - we are reading something from the device or this device is telling us something.
Write to device
We have a device, we read information from it, we manage it. So, we can write information to it, as a rule, - the usual NSData. Only it is necessary to find out what this device expects from us and what will be accepted by it.
Most BLE devices come with a specification, a kind of API, from which it is clear how to “communicate” with them. You can pull the data from the characteristics to get at least a rough idea of what the device expects from us.
From the specifications we find out in which characteristics which properties we read, and in which we write, whether we will be notified of changes (isNotifying). Most often here we will find everything that is required for work.
peripheral.writeValue(data: NSData!, forCharacteristic: CBCharacteristic!, type: CBCharacteristicWriteType) characteristic.properties - OptionSet type characteristic.isNotifying func peripheral(peripheral: CBPeripheral!, didWriteValueForCharacteristic characteristic: CBCharacteristic!, error: NSError!)
During the writing process, the delegate will let us know that everything went well (didWriteValueForCharacteristics) that the required value was updated, and we can tell the user about it or use this information differently.
We consider the topic in a very narrow cut, relying on the implementation of Apple, so there are a number of problems that will have to be faced. For example, a very strong dependence on the delegation, so beloved Apple.
Inheritance of CBPeripheral? If everything was so easy
It would seem that since we have a device, we can start using it, but in fact it will not tell us anything about ourselves. Perhaps we want to control the lock, air conditioning or pulse sensor. You need to know which device we are communicating with.
It looks like inheritance: we have a special case of something in common. From my experience, I can say that when using inheritance, something will not work at all as expected, something will not work at all, and you will not know why. In general, I would caution you against the idea of inheriting CBPeripheral. What to do?
I advise you to add CBPeripheral to the constructor of the object that will manage it. It encapsulates it inside this class. Use it to interact with the device, hold a strong link to it, so that iOS does not break the connection. But the most important thing is that this class will be used as a delegate, otherwise it will be difficult to manage all devices in one place, this threatens with a bunch of if else.
Connect and work with CBPeripheralDelegate
And here we are connecting to the device and want to be CBPeripheralDelegate. There is one more nuance: while you are working with the device, “interrogate” its services and characteristics, read and write to them, almost all communication takes place with the periferal. Everything except the connection.
Naturally, we would like to concentrate all communication in one place, but the manager should be aware of what is happening with the device. And the difficulty is to have one source of truth, to make sure that everyone is timely informed about what is happening with the device. To do this, we will monitor the state of the peripheral - it can change from disconnected (disconnected), connected (connecting) and connected (connected). It will always tell you about the current situation. It remains to subscribe to the status change in our management facility, which I mentioned earlier, this will give the opportunity to communicate with the device from one place.
Determination of proximity
A very important point, since finding normal documentation on this topic is difficult. In the case of Apple and their iBeacons, everything is simple, they tell us how close we are to the bluetooth device.
Unfortunately, we are not given such an easy way to work with third-party devices. And more than once it happened that there was a need to determine the nearest device. It is also difficult to understand whether the device is in the available range or not. Sometimes when searching for devices, it can make itself known only once and disappear, then the connection attempt will be unsuccessful.
We use the following method: save the stack with date and signal level labels (RSSI) of each message received in discoverPeripheral. If someone came across CoreLocation, our method is similar to how time stamps and corresponding coordinates are stored there. Usually, the higher the signal (RSSI), the closer the device. To understand whether the device is in the available range or not is more difficult, partly because this concept itself is quite flexible. For this, I use the weighted average of the signal. Keep in mind that the signal level of a connected device must be manually requested every time you need to know it.
Unfortunately, this article will not make you an expert, if you read it and it
became interesting to you - pay attention to Apple's CoreBluetooth Programming Guide , the manual is not very big, but very useful. There is still a couple of broadcasts from WWDC 2012 ( basic and advanced ) and one since 2013 , but don't worry, not much has changed since then.
There is also a video from Altconf 2015 posted on the Realm website, where John Shier, an excellent guy and specialist, shares his experience.