Nowadays it is very common to have contact with wireless devices. Say a headphone, a mouse, a keyboard, or the famous Apple Watch. The list is long… However, most of them use basically the same technology that makes them indeed wireless devices: Bluetooth®.
Here we are going to learn briefly the roles and the exchanged properties via a Bluetooth connection, and the execution modes using the Core Bluetooth framework, that gives us the support of using the version 4.0 (a.k.a smart, a.k.a. low-energy).
First off, we need to talk about the execution modes…
Core Bluetooth (or CB) gives us two execution modes, Foreground and Background. This way, we can define if we want our app to be “connectable” when only the user is interacting with it or not. Some aspects define properly these modes, they are:
If the app is minimized or another app becomes the primary one used by the user, our Bluetooth support goes along. So in the suspended state your app is not able to perform tasks related to Bluetooth, nor is it aware of events related to Bluetooth. However, the list of events received while the app was in background is stacked and these events are delivered when the user resumes the app, making it active again.
We will explain below the difference between the roles and the properties, but in this mode if the application is not active, the peripheral cannot send advertisements (advertising) and the central cannot broadcast its signal (scanning), of the execution of advertising and scanning are answered on demand, virtually without delays.
The advantage of using the Foreground mode is that we have all the system resources at our fingertips, such as updating views and tasks with no timeout. Still in this mode, our stack of devices is well off, even having the option to choose if we want to scan the same peripheral for some specific reason. This is done by using a Core Bluetooth settings property called CBCentralManagerScanOptionAllowDuplicatesKey
, which is ignored in Background mode, just as other properties are treated differently when executed in Foreground and Background modes and can be found in References.
Here we start by saying that system resources are limited. We also have 10 seconds maximum to execute a task in this mode. But I think that’s more than enough. The runtime of advertising and scanning may be longer. This is done in order to save battery power from your device. Here the events from the same device are combined into a single event with their Services and Characteristics, that is, the option to scan the same device more than once is discarded.
The advantage of this mode is that our app doesn’t need to be active to exchange data between centrals and peripherals. The system wakes up our app from a suspended state, allowing you to read, write, subscribe to features and listen to events, all in background. But even if we are allowed to run on background, our app might not be always running. This may happen because eventually the system may terminate our app to free up memory for the app that is in the active state. As of iOS 7, it is allowed to save device states and pending connections before an interruption and then restore them afterwards.
To picture this state restoration, let’s imagine that you have developed a security device that communicates with an electronic lock equipped with Bluetooth. The app and the lock can interact so that the lock locks automatically when the user leaves the house and unlocks when the user returns. All of this with the iPhone in the pocket. When the user leaves the house, the iPhone can eventually get out of reach of the lock and then lose connection. In this case, the user can simply call the function to connect the device even though it is not within range and thus the connection will be redone when they are within range. This happens because the device connection request does not expire.
Now imagine that the user is away from home on a trip. If the app is terminated by the system while the user is away, the app will not be able to reconnect with the lock when the user returns. So the lock will not unlock. For apps like these, it is definitely necessary to use the save and restore state option.
Note: macOS and iOS work a little differently on peripheral and central roles. For example, macOS does not accept the flag CBCentralManagerOptionRestoreIdentifierKey
, because in theory macOS background does not end apps like iOS does.
To make use of Background mode, we still need to define flags in Info.plist
.
bluetooth-peripheral
: If we want to use a peripheral in background;bluetooth-central
: If we want to use a central in background.If we add support via Target > Capabilities > Background Modes
, by selecting Uses Bluetooth LE accessories (central)
and/or Act as Bluetooth LE accessory (peripheral)
, they are automatically added to Info.plist
.
Important: Do not forget to add the flag Privacy Flag - Bluetooth Peripheral Usage Description
so that the user gets asked for permission for the app to use the device as a peripheral, just as we do when we want to use the camera or location.
In this set of Bluetooth connections, we have two distinct well-specified roles that work as a client-server approach. They are: Peripheral and Central.
A peripheral is the device that shares its data with the central. In this mode, the system wakes up our app to process reading, writing and logging events from the central. This is the device that works as a Server, which means that it has the data that is being wanted.
When a peripheral is running in Background, its services are reallocated to a special “overflow” area, that is, all UUIDs of the services contained in the CBAdvertisementDataServiceUUIDsKey
property value. Therefore, it can only be discovered by a device that is explicitly scanning by its information looking for some of its characteristic, basically its identifier.
A central one is the device that request information from peripheral devices. In this case, it scans, connects, retrieves data, and sends data. It is the Client.
The system wakes up our central when the events of state changing occur with the peripheral, such as connection established or cut with the peripheral, when peripheral updates information of its characteristics, or even when our central is close to being ended and also restored.
Imagine the functioning of a heart rate monitor of runner. The peripheral would be the metering device that is located below the runner chest. An app on iPhone could read this value and show it to the user in real time, making iPhone work as central in an action on Foreground. Now imagine that the runner will not look at their iPhone all the time, the iPhone captures in Background mode the heart beat data and saves in a log where the user can check for variations when the workout is finished.
Central and Peripheral
Image: CoreBluetooth Documentation
The connection between a central and a peripheral is made through advertisement and scanning. Basically this is the flow:
Advertising and Scanning
Image: CoreBluetooth Documentation
The data exchanging by connected devices are made through services and features already discussed in some topics previously.
A service is a collection of data and associated behaviors to complete a task or function of a device, or parts of a device. These behaviors are called characteristics. A feature provides more detail about a peripheral service. It basically looks like a description of those entries that you look for in a dictionary. One setting takes you to another and you stay in an eternal loop. But let’s try to explain it better below.
Services may have other related services, such as dependencies, pointing to UUIDs of other services. Each characteristic also has UUID that it can be identified by.
Services and Characteristics
Image: CoreBluetooth Documentation
So in this example we have two sensors that work differently from each other but together they produce a service, which is Heart Rate. For it to work properly, the cardiac sensor must be positioned in the ideal location.
Still using our previous example, suppose that we could have two services in the device of heart beat, one with two characteristics and one with only one:
But a peripheral is nothing if it is not sending advertisements. An advertisement package is relatively a small set of data that has information about the peripheral device for an initial recognition. Data such as device name, UUID and RSSI, which is information on how strong the peripheral signal is.
We will exemplify here the following:
To check the background mode, check out the repository of this project in GitHub in the branch background.
First, let’s create a protocol to receive the events listened from the BluetoothManager
and update the views of our ViewController
’s. Let’s call it BlueEar
. And it has one version for Central and another for Peripheral. Like BlueEar, we will have a class that will be the manager of our Bluetooth connection and it is the BluetoothManager
.
protocol BlueEar {
func didStartConfiguration()
func didStartAdvertising()
func didSendData()
func didReceiveData()
}
class BluetoothManager: NSObject {
// MARK: - Properties
let peripheralId: String = "62443cc7-15bc-4136-bf5d-0ad80c459215"
let serviceUUID: String = "0cdbe648-eed0-11e6-bc64-92361f002671"
let characteristicUUID: String = "199ab74c-eed0-11E6-BC64-92361F002672"
let localName: String = "Peripheral - iOS"
let properties: CBCharacteristicProperties = [.read, .notify, .writeWithoutResponse, .write]
let permissions: CBAttributePermissions = [.readable, .writeable]
var bluetoothMessaging: BlueEar?
var peripheralManager: CBPeripheralManager?
var serviceCBUUID: CBUUID?
var characteristicCBUUID: CBUUID?
var service: CBMutableService?
var characterisctic: CBMutableCharacteristic?
// MARK: - Initializers
convenience init (delegate: BlueEar?) {
self.init()
self.bluetoothMessaging = delegate
guard
let serviceUUID: UUID = NSUUID(uuidString: self.serviceUUID) as UUID?,
let characteristicUUID: UUID = NSUUID(uuidString: self.characteristicUUID) as UUID?
else { return }
self.serviceCBUUID = CBUUID(nsuuid: serviceUUID)
self.characteristicCBUUID = CBUUID(nsuuid: characteristicUUID)
guard
let serviceCBUUID: CBUUID = self.serviceCBUUID,
let characteristicCBUUID: CBUUID = self.characteristicCBUUID
else { return }
// Configuring service
self.service = CBMutableService(type: serviceCBUUID, primary: true)
// Configuring characteristic
self.characterisctic = CBMutableCharacteristic(type: characteristicCBUUID, properties: self.properties, value: nil, permissions: self.permissions)
guard let characterisctic: CBCharacteristic = self.characterisctic else { return }
// Add characterisct to service
self.service?.characteristics = [characterisctic]
self.bluetoothMessaging?.didStartConfiguration()
// Initiate peripheral and start advertising
self.peripheralManager = CBPeripheralManager(delegate: self, queue: nil, options: nil)
}
}
// MARK: - CBPeripheralManagerDelegate
extension BluetoothManager: CBPeripheralManagerDelegate {
func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
print("peripheralManagerDidUpdateState")
if peripheral.state == .poweredOn {
guard let service: CBMutableService = self.service else { return }
self.peripheralManager?.removeAllServices()
self.peripheralManager?.add(service)
}
}
func peripheralManager(_ peripheral: CBPeripheralManager, didAdd service: CBService, error: Error?) {
print("\ndidAdd service")
let advertisingData: [String: Any] = [
CBAdvertisementDataServiceUUIDsKey: [self.service?.uuid],
CBAdvertisementDataLocalNameKey: "Peripheral - iOS"
]
self.peripheralManager?.stopAdvertising()
self.peripheralManager?.startAdvertising(advertisingData)
}
func peripheralManagerDidStartAdvertising(_ peripheral: CBPeripheralManager, error: Error?) {
print("peripheralManagerDidStartAdvertising")
self.bluetoothMessaging?.didStartAdvertising()
}
// Listen to dynamic values
// Called when CBPeripheral .setNotifyValue(true, for: characteristic) is called from the central
func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didSubscribeTo characteristic: CBCharacteristic) {
print("\ndidSubscribeTo characteristic")
guard let characterisctic: CBMutableCharacteristic = self.characterisctic else { return }
do {
// Writing data to characteristics
let dict: [String: String] = ["Hello": "Darkness"]
let data: Data = try PropertyListSerialization.data(fromPropertyList: dict, format: .binary, options: 0)
self.peripheralManager?.updateValue(data, for: characterisctic, onSubscribedCentrals: [central])
self.bluetoothMessaging?.didSendData()
} catch let error {
print(error)
}
}
// Read static values
// Called when CBPeripheral .readValue(for: characteristic) is called from the central
func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveRead request: CBATTRequest) {
print("\ndidReceiveRead request")
if let uuid: CBUUID = self.characterisctic?.uuid, request.characteristic.uuid == uuid {
print("Match characteristic for static reading")
}
}
// Called when receiving writing from Central.
func peripheralManager(_ peripheral: CBPeripheralManager, didReceiveWrite requests: [CBATTRequest]) {
print("\ndidReceiveWrite requests")
guard
let characteristicCBUUID: CBUUID = self.characteristicCBUUID,
let request: CBATTRequest = requests.filter({ $0.characteristic.uuid == characteristicCBUUID }).first,
let value: Data = request.value
else { return }
// Send response to central if this writing request asks for response [.withResponse]
print("Sending response: Success")
self.peripheralManager?.respond(to: request, withResult: .success)
print("Match characteristic for writing")
do {
if let receivedData: [String : String] = try PropertyListSerialization.propertyList(from: value, options: [], format: nil) as? [String: String] {
print("Written value is: \(receivedData)")
self.bluetoothMessaging?.didReceiveData()
} else {
return
}
} catch let error {
print(error)
}
}
func peripheralManager(_ peripheral: CBPeripheralManager, central: CBCentral, didUnsubscribeFrom characteristic: CBCharacteristic) {
print("\ndidUnsubscribeFrom characteristic")
}
func peripheralManager(_ peripheral: CBPeripheralManager, willRestoreState dict: [String : Any]) {
print("willRestoreState")
}
func peripheralManagerIsReady(toUpdateSubscribers peripheral: CBPeripheralManager) {
print("peripheralManagerIsReady")
}
}
import UIKit
class ViewController: UIViewController {
// MARK: - IBOutlet
@IBOutlet var label: UILabel!
// MARK: - Life Cycle
override var preferredStatusBarStyle: UIStatusBarStyle { return .lightContent }
// MARK: - Properties
var manager: BluetoothManager?
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.manager = BluetoothManager(delegate: self)
}
}
// MARK: - BlueEar
extension ViewController: BlueEar {
func didStartConfiguration() { self.label.text = "Start configuration 🎛" }
func didStartAdvertising() { self.label.text = "Start advertising 📻" }
func didSendData() { self.label.text = "Did send data ⬆️" }
func didReceiveData() { self.label.text = "Did received data ⬇️" }
}
Notes:
peripheralManagerDidUpdateState(: CBPeripheralManager)
function is actually called and the peripheral’s state is .poweredOn
;.setNotifyValue(true, for: characteristic)
function fires the peripheral’s peripheralManager(_: CBPeripheralManager, : CBCentral, : CBCharacteristic)
function; and the reading of static values made by the Central through the .readValue(for: characteristic)
function fires peripheral’s peripheralManager(_: CBPeripheralManager, :CBATTRequest)
.
protocol BlueEar {
func didStartConfiguration()
func didStartScanningPeripherals()
func didConnectPeripheral(name: String?)
func didDisconnectPeripheral(name: String?)
func didSendData()
func didReceiveData()
func didFailConnection()
}
class BluetoothManager: NSObject {
// MARK: - Properties
let serviceUUID: String = "0cdbe648-eed0-11e6-bc64-92361f002671"
let characteristicUUID: String = "199ab74c-eed0-11e6-bc64-92361f002672"
var serviceCBUUID: CBUUID?
var characteristicCBUUID: CBUUID?
var blueEar: BlueEar?
var centralManager: CBCentralManager?
var discoveredPeripheral: CBPeripheral?
// MARK: - Initializers
convenience init (delegate: BlueEar) {
self.init()
self.blueEar = delegate
guard
let serviceUUID: UUID = NSUUID(uuidString: self.serviceUUID) as UUID?,
let characteristicUUID: UUID = NSUUID(uuidString: self.characteristicUUID) as UUID?
else { return }
self.serviceCBUUID = CBUUID(nsuuid: serviceUUID)
self.characteristicCBUUID = CBUUID(nsuuid: characteristicUUID)
}
// MARK: - Functions
func scan() {
self.centralManager = CBCentralManager(delegate: self, queue: nil, options: nil)
self.blueEar?.didStartConfiguration()
}
}
// MARK: - CBCentralManagerDelegate
extension BluetoothManager: CBCentralManagerDelegate {
func centralManagerDidUpdateState(_ central: CBCentralManager) {
print("\ncentralManagerDidUpdateState \(Date())")
if central.state == .poweredOn {
guard let serviceCBUUID: CBUUID = self.serviceCBUUID else { return }
self.blueEar?.didStartScanningPeripherals()
self.centralManager?.scanForPeripherals(withServices: [serviceCBUUID], options: nil)
}
}
func centralManager(_ central: CBCentralManager, didDiscover peripheral: CBPeripheral, advertisementData: [String : Any], rssi RSSI: NSNumber) {
// We must keep a reference to the new discovered peripheral, which means we must retain it.
self.discoveredPeripheral = peripheral
print("\ndidDiscover:", self.discoveredPeripheral?.name ?? "")
self.discoveredPeripheral?.delegate = self
guard let discoveredPeripheral: CBPeripheral = self.discoveredPeripheral else { return }
self.centralManager?.connect(discoveredPeripheral, options: nil)
}
func centralManager(_ central: CBCentralManager, didConnect peripheral: CBPeripheral) {
print("\ndidConnect", self.discoveredPeripheral?.name ?? "")
self.blueEar?.didConnectPeripheral(name: peripheral.name ?? "")
guard let serviceCBUUID: CBUUID = self.serviceCBUUID else { return }
self.discoveredPeripheral?.discoverServices([serviceCBUUID])
}
func centralManager(_ central: CBCentralManager, willRestoreState dict: [String : Any]) {
print("willRestoreState")
}
func centralManager(_ central: CBCentralManager, didRetrievePeripherals peripherals: [CBPeripheral]) {
print("\ndidRetrievePeripherals")
}
func centralManager(_ central: CBCentralManager, didFailToConnect peripheral: CBPeripheral, error: Error?) {
print("\ndidFailToConnect")
self.blueEar?.didFailConnection()
}
func centralManager(_ central: CBCentralManager, didRetrieveConnectedPeripherals peripherals: [CBPeripheral]) {
print("\ndidRetrieveConnectedPeripherals")
}
func centralManager(_ central: CBCentralManager, didDisconnectPeripheral peripheral: CBPeripheral, error: Error?) {
print("\ndidDisconnectPeripheral", self.discoveredPeripheral?.name ?? "")
self.blueEar?.didDisconnectPeripheral(name: peripheral.name ?? "")
}
}
// MARK: - CBPeripheralDelegate
extension BluetoothManager: CBPeripheralDelegate {
func peripheral(_ peripheral: CBPeripheral, didDiscoverServices error: Error?) {
print("\ndidDiscoverServices")
if let service: CBService = self.discoveredPeripheral?.services?.filter({ $0.uuid == self.serviceCBUUID }).first {
guard let characteristicCBUUID: CBUUID = self.characteristicCBUUID else { return }
self.discoveredPeripheral?.discoverCharacteristics([characteristicCBUUID], for: service)
}
}
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor characteristic: CBCharacteristic, error: Error?) {
print("\ndidWriteValueFor \(Date())")
// After we write data on peripheral, we disconnect it.
self.centralManager?.cancelPeripheralConnection(peripheral)
// We stop scanning.
self.centralManager?.stopScan()
}
func peripheral(_ peripheral: CBPeripheral, didDiscoverCharacteristicsFor service: CBService, error: Error?) {
print("\ndidDiscoverCharacteristicsFor")
if let characteristic: CBCharacteristic = service.characteristics?.filter({ $0.uuid == self.characteristicCBUUID }).first {
print("Matching characteristic")
// To listen and read dynamic values
self.discoveredPeripheral?.setNotifyValue(true, for: characteristic)
// To read static values
// self.discoveredPeripheral?.readValue(for: characteristic)
}
}
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor characteristic: CBCharacteristic, error: Error?) {
print("\ndidUpdateValueFor")
// We read
if let value: Data = characteristic.value {
do {
let receivedData: [String: String] = try PropertyListSerialization.propertyList(from: value, options: [], format: nil) as! [String: String]
print("Value read is: \(receivedData)")
self.blueEar?.didReceiveData()
} catch let error {
print(error)
}
}
// We write
do {
print("\nWriting on peripheral.")
let dict: [String: String] = ["Yo": "Lo"]
let data: Data = try PropertyListSerialization.data(fromPropertyList: dict, format: .binary, options: 0)
self.discoveredPeripheral?.writeValue(data, for: characteristic, type: .withResponse)
self.blueEar?.didSendData()
} catch let error {
print(error)
}
}
func peripheralDidUpdateRSSI(_ peripheral: CBPeripheral, error: Error?) {
print("\nperipheralDidUpdateRSSI")
print(self.discoveredPeripheral?.rssi ?? "")
}
func peripheralDidUpdateName(_ peripheral: CBPeripheral) {
print("\nperipheralDidUpdateName")
}
func peripheral(_ peripheral: CBPeripheral, didWriteValueFor descriptor: CBDescriptor, error: Error?) {
print("\ndidWriteValueFor")
}
func peripheral(_ peripheral: CBPeripheral, didModifyServices invalidatedServices: [CBService]) {
print("\ndidModifyServices")
}
func peripheral(_ peripheral: CBPeripheral, didUpdateValueFor descriptor: CBDescriptor, error: Error?) {
print("\ndidUpdateValueFor")
}
func peripheral(_ peripheral: CBPeripheral, didDiscoverIncludedServicesFor service: CBService, error: Error?) {
print("\ndidDiscoverIncludedServicesFor")
}
func peripheral(_ peripheral: CBPeripheral, didDiscoverDescriptorsFor characteristic: CBCharacteristic, error: Error?) {
print("\ndidDiscoverDescriptorsFor")
}
func peripheral(_ peripheral: CBPeripheral, didUpdateNotificationStateFor characteristic: CBCharacteristic, error: Error?) {
print("\ndidUpdateNotificationStateFor")
}
}
import Foundation
import Cocoa
class ViewController: NSViewController {
// MARK: - IBOutlet
@IBOutlet weak var label: NSTextField!
@IBOutlet weak var button: NSButton!
// MARK: - Properties
var manager: BluetoothManager?
// MARK: - Life Cycle
@IBAction func discover(_ sender: Any) {
self.manager = BluetoothManager(delegate: self)
self.manager?.scan()
}
}
// MARK: - BlueEar
extension ViewController: BlueEar {
func didStartConfiguration() { self.label.stringValue = "Start configuration 🎛" }
func didStartScanningPeripherals() { self.label.stringValue = "Start scanning peripherals 👀" }
func didConnectPeripheral(name: String?) { self.label.stringValue = "Did connect to: \(name ?? "") 🤜🏽🤛🏽" }
func didDisconnectPeripheral(name: String?) { self.label.stringValue = "Did disconnect: \(name ?? "") 🤜🏽🤚🏽" }
func didSendData() { self.label.stringValue = "Did send data ⬆️" }
func didReceiveData() { self.label.stringValue = "Did received data ⬇️" }
func didFailConnection() { self.label.stringValue = "Connection failed 🤷🏽♂️" }
}
Notes:
centralManagerDidUpdateState(: CBCentralManager)
function is actually called and the central’s state is .poweredOn
;CBPeripheralDelegate
is different from CBPeripheralManagerDelegate
that was used on the peripheral implementation. Both of them are indeed related to the peripheral, but the CBPeripheralDelegate
allows the central to scan events occurring in the peripheral;Data
format, so you have to transmit whatever you want to that type of data, so it can be correctly received in the other end.