Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 26 additions & 21 deletions iOS/Posts/Getting-Started-with-Core-Bluetooth.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,27 @@ When working with Core Bluetooth, it is necessary to become accustomed with the

### Centrals and Peripherals

Bluetooth operates in a very traditional server/client sort of model. One device acts in a child capacity that produces information, and another acts as the overarching parent that receives this information and decides how to act upon it.
Bluetooth operates in a very traditional server/client sort of model. One device acts in the capacity of a server that produces information, and another acts as a client that queries for this information and processes/displays it locally.

In Bluetooth, these parent devices are referred to as “centrals” and child devices are referred to as “peripherals”. In most traditional Core Bluetooth setups, the iOS device will almost always be the central, and any BLE devices paired to the iOS device will act as a peripheral.
In Bluetooth, accessory devices that produce and store data are called "peripherals" and devices interested in accessing that data are called "centrals". In a traditional scenario, when two devices are wanting to connect to each other, the peripheral will broadcast an advertisement about itself, and the central will be scanning for that advertisement. As a general example, when pairing a pair of Bluetooth headphones to a smartphone, the smartphone would be the central, and the headphones would be the peripheral.

(Insert a picture of an iPhone and a BLE device, labeling them as such).
(Insert a picture of an iPhone and a BLE device, labelling them as such).

One very important thing to note. With the given parent/child terminology, it's very easy to assume that centrals act as the server, and peripherals act as the clients.

However, as it turns out, this terminology is really only useful when designating the role of each device when connecting. One needs to be advertising, and one needs to be scanning.

Once a connection is formed, there is no longer any specific parent/child hierarchal relationship between the devices and both can behave as servers sending data between each other.

### Services

Obviously, depending on the type of BLE device in question will determine what sort of capabilities it has. For example, a heart rate monitor can record a wearer’s heart rate, but a smart thermostat would record the current temperature of its location.

Most apps will be built to support one or more specific class of device capability. For example, apps for tracking a users health would have no interest in connecting to devices that aren't health related, such as a thermostat. In order to encapsulate and report on the capabilities of specific peripherals, Bluetooth requires that peripherals identify their classes or capabilities as “services”.

Peripherals will make themselves discoverable to centrals by broadcasting advertisement packets containing information on the services they support. When a central scanning at the same time detects these packets and determines that the peripheral device it found supports the same services as it does, then the two devices recognize they are compatible with each other and can begin the process of connecting to each other.
Peripherals will make themselves discoverable to centrals by broadcasting advertisement packets defining the main identifying service they offer. When a central scanning at the same time detects these packets and determines that the peripheral device it found supports the same class of service that it is searching for, then the central can determine that they it is compatible with the peripheral and can begin the process of connecting to each other. Traditionally, while only the main service of the device is advertised, once a connection is formed, the central can then query the peripheral for any additional services it offers.

In order for centrals and peripherals to be able to recognize each others’ supported services as matching, it is necessary for the ID values of these services to match. For very specific apps and peripheral devices, it makes sense to define a service using a shared UUID between both devices.
In order for the central to determine it is compatible with a peripheral, it needs to know the ID values of the services that the peripheral supports. For very specific apps and peripheral devices, it makes sense to define a service using a shared UUID between both devices.

However, in more general practice, it makes sense for peripherals to adopt Bluetooth services that are a standard capability globally. For example, it would make sense that any device that records blood pressure readings could be connected to *any* Bluetooth device capable of processing that data, regardless of whoever manufactured either device. As such, for common standards, [a public database exists](https://www.bluetooth.com/specifications/gatt/services/) that lists a standardized set of service IDs that can be used between both centrals and peripherals who want to adopt a specific use case.

Expand Down Expand Up @@ -93,7 +99,7 @@ let centralManager = CBCentralManager(delegate: self, queue: nil)

As you can see, an object must be designated as a delegate upon instantiation. This object must conform to `CBCentralManagerDelegate`, and upon instantiation of this central manager, all of the necessary activity needed to start using Bluetooth is started immediately.

Unfortunately, at this point, we can't start advertising yet. Bluetooth spends a non-trivial amount of time setting itself up to a state it could be considered as "powered on", so immediately after this, we must wait for the first delegate callback.
At this point, we can't start scanning yet. Bluetooth spends a non-trivial amount of time setting itself up to a state it could be considered as "powered on", so immediately after this, we must wait for the first delegate callback.

```swift
func centralManagerDidUpdateState(_ central: CBCentralManager) {
Expand All @@ -107,11 +113,11 @@ func centralManagerDidUpdateState(_ central: CBCentralManager) {
Scanning for peripherals is very easy. We just need to call `scanForPeripherals` and specify the services we are interested in.

```swift
let service = CBUUID(string: "AAAA")
let service = CBUUID(string: "9f37e282-60b6-42b1-a02f-7341da5e2eba")
centralManager.scanForPeripherals(withServices: [service], options: nil]
```

As mentioned above, services carry unique identifiers so peripherals and centrals may match them. In Core Bluetooth, these identifications are handled via the `CBUUID` object, and for this specific case, we can use a simple string as the identifier.
As mentioned above, services carry unique identifiers so peripherals and centrals may match them. In Core Bluetooth, these identifications are handled via the `CBUUID` object, and for this specific case, we can use a simple string as the identifier. For this tutorial, I'm using a UUID string value generated from [Online UUID Generator](https://www.uuidgenerator.net). The value needs to be globally unique, but recognizable from both peripheral and central sides.

At this point, the device will now be scanning for peripherals with the same matching service identifier. At any point, a central can be checked if it is scanning by calling `centralManager.isScanning`.

Expand All @@ -122,7 +128,7 @@ Now that our central is scanning, we need another device acting as a peripheral
Similar to how centrals are managed via a `CBCentralManager`, peripherals are managed by instances of `CBPeripheralManager`.

```swift
peripheralManager = CBPeripheralManager(delegate: self, queue: nil)
let peripheralManager = CBPeripheralManager(delegate: self, queue: nil)
```

And exactly the same as central managers, peripheral managers require a delegate upon creation (This time conforming to `CBPeripheralManagerDelegate`) that also must wait for the state of Bluetooth on the device to reach "powered on".
Expand All @@ -139,13 +145,13 @@ Once the state of the Bluetooth peripheral is powered on, the peripheral can the

```swift

let characteristicID = CBUUID(string: "BBBB")
let characteristicID = CBUUID(string: "890aa912-c414-440d-88a2-c7f66179589b")

// Create and configure our characteristic
let characteristic = CBMutableCharacteristic(type: characteristicID, properties: [.write, .notify], value: nil, permissions: .writeable)

// Create our service, and add our characteristic to it
let serviceID = CBUUID(string: "AAAA")
let serviceID = CBUUID(string: "9f37e282-60b6-42b1-a02f-7341da5e2eba")
let service = CBMutableService(type: serviceID, primary: true)
service.characteristics = [characteristic]

Expand Down Expand Up @@ -183,7 +189,7 @@ func centralManager(_ centralManager: CBCentralManager, didDiscover peripheral:
}
```

`didDiscoverPeripheral` will provide a lot of interesting information about the peripheral. The `advertismentData` dictionary will contain information about it like the devices name and manufacturer name, in addition to all of the service UUIDs it supports.
`didDiscoverPeripheral` will provide a lot of interesting information about the peripheral. The `advertismentData` dictionary will contain information about it like the devices name and manufacturer name, in addition to all of the service UUIDs that were defined in `CBAdvertisementDataServiceUUIDsKey` (Though there may be more we can discover later).

If necessary, it's possible to check if this peripheral supports the services this central wants by checking the value of
`advertisementData[CBAdvertisementDataServiceUUIDsKey]`. In addition, the RSSI value ([Received Signal Strength Indicator](https://en.wikipedia.org/wiki/Received_signal_strength_indication)) is useful in determining the distance of the peripheral. Sometimes it may be necessary to require a specific proximity for proper functioning, and this value can be used to monitor for that.
Expand All @@ -202,7 +208,7 @@ func centralManager(_ centralManager: CBCentralManager, didConnect peripheral: C
// Configure a delegate for the peripheral
peripheral.delegate = self

let service = CBUUID(string: "AAAA")
let service = CBUUID(string: "9f37e282-60b6-42b1-a02f-7341da5e2eba")
// Scan for the chat characteristic we'll use to communicate
peripheral.discoverServices([service])
}
Expand All @@ -218,14 +224,15 @@ Once we've set ourselves to be the delegate of a peripheral and performed the re

```swift
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
// If an error occurred, print it, and then return
// If an error occurred, disconnect so we can try again from the start
if let error = error {
print("Unable to discover services: \(error.localizedDescription)")
cleanUp()
return
}

// Specify the characteristic we want
let characteristic = CBUUID("BBBB")
let characteristic = CBUUID("890aa912-c414-440d-88a2-c7f66179589b")

// It's possible there may be more than one service, so loop through each one to discover the one that we want
peripheral.services?.forEach { service in
Expand All @@ -247,14 +254,15 @@ From above, once the characteristics of a service have been discovered, the foll

```swift
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
// Handle if any errors occurred
// If an error occurred, disconnect so we can try again from the start
if let error = error {
print("Unable to discover characteristics: \(error.localizedDescription)")
cleanUp()
return
}

// Specify the characteristic we want
let characteristicUUID = CBUUID("BBBB")
let characteristicUUID = CBUUID("890aa912-c414-440d-88a2-c7f66179589b")

// Perform a loop in case we received more than one characteristic
service.characteristics?.forEach { characteristic in
Expand Down Expand Up @@ -282,7 +290,7 @@ One of the final steps in the process is that the peripheral will report whether
func peripheral(_ peripheral: CBPeripheral,
didUpdateNotificationStateFor characteristic: CBCharacteristic,
error: Error?) {
// Perform any error handling if one occurred
// Perform any error handling if one occurred. It's not necessary to abandon the connection from this kind of error
if let error = error {
print("Characteristic update notification failed: \(error.localizedDescription)")
return
Expand Down Expand Up @@ -389,9 +397,6 @@ Now that we've discussed the Core Bluetooth API and its design pattern, it shoul

The folks here at Ditto use Core Bluetooth in their flagship product, and by extension, Bluetooth Low Energy itself for Android support. In addition to some of the challenges and limitations I experienced in this project, here are some of the challenges the Ditto engineers have faced as well.

### Asymmetric Connections
As mentioned above, Bluetooth operates in a very traditional client/server model with regards to centrals and peripherals. In scenarios where this model makes sense, this is fine, but like our chat app, where ideally both devices should be identical, this comes up as a limitation. With enough effort however, it is possible to build an abstraction on top of this that makes the system perform like a traditional 2-way stream.

### Limited Message Sizes
One thing I completely glossed over is that the amount of data that can be sent through a characteristic has a very hard limit, and that limit changes between devices. Historically, it's been 20 bytes, but on more modern phone hardware, it can be around 180 bytes. For a chat app where the payload is very small per message, it isn't so much of a concern, but it certainly is something that a production app needs to take seriously. Core Bluetooth is capable of detecting and reporting the acceptable length of each message, and if a device wants to send more than that, then it's the responsibility of your own code to chunk that data up and send it as multiple messages.

Expand Down